Commit be6cf63d authored by Mygod's avatar Mygod

Merge branch 'master' into preference-1.1

parents f4171711 dca0da09
......@@ -9,14 +9,11 @@ buildscript {
sdkVersion = 28
compileSdkVersion = 28
buildToolsVersion = '28.0.3'
lifecycleVersion = '2.0.0'
roomVersion = '2.0.0'
workVersion = '1.0.0-beta05'
junitVersion = '4.12'
androidTestVersion = '1.1.1'
androidEspressoVersion = '3.1.1'
versionCode = 4070050
versionName = '4.7.0-nightly'
versionCode = 4070150
versionName = '4.7.1-nightly'
}
repositories {
......@@ -28,6 +25,7 @@ buildscript {
}
dependencies {
classpath 'com.vanniktech:gradle-maven-publish-plugin:0.8.0'
classpath 'com.android.tools.build:gradle:3.3.1'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.20.0'
classpath 'com.google.gms:google-services:4.2.0'
......
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
......@@ -38,6 +39,13 @@ android {
}
}
androidExtensions {
experimental = true
}
def lifecycleVersion = '2.0.0'
def roomVersion = '2.0.0'
def workVersion = '1.0.0-rc02'
dependencies {
api project(':plugin')
api "android.arch.work:work-runtime-ktx:$workVersion"
......@@ -46,7 +54,7 @@ dependencies {
api 'androidx.preference:preference:1.1.0-alpha03'
api "androidx.room:room-runtime:$roomVersion"
api 'com.crashlytics.sdk.android:crashlytics:2.9.9'
api 'com.google.firebase:firebase-config:16.3.0'
api 'com.google.firebase:firebase-config:16.1.3'
api 'com.google.firebase:firebase-core:16.0.7'
api 'dnsjava:dnsjava:2.1.8'
api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
......
#
# GFW list from https://github.com/gfwlist/gfwlist/blob/master/gfwlist.txt
# updated on 2018-03-30
# updated on 2019-02-21
#
[bypass_all]
......@@ -14,6 +14,8 @@
91.108.56.0/22
109.239.140.0/24
149.154.160.0/20
1.1.1.1
14.102.250.18
14.102.250.19
174.142.105.153
......@@ -36,6 +38,7 @@
(^|\.)12bet\.com$
(^|\.)12vpn\.com$
(^|\.)12vpn\.net$
(^|\.)138\.com$
(^|\.)141hongkong\.com$
(^|\.)141jj\.com$
(^|\.)141tube\.com$
......@@ -72,7 +75,6 @@
(^|\.)24hrs\.ca$
(^|\.)24smile\.org$
(^|\.)25u\.com$
(^|\.)2dbook\.com$
(^|\.)2lipstube\.com$
(^|\.)2shared\.com$
(^|\.)2waky\.com$
......@@ -88,6 +90,7 @@
(^|\.)3proxy\.ru$
(^|\.)3ren\.ca$
(^|\.)3tui\.net$
(^|\.)43110\.cf$
(^|\.)466453\.com$
(^|\.)4bluestones\.biz$
(^|\.)4chan\.com$
......@@ -98,11 +101,11 @@
(^|\.)4pu\.com$
(^|\.)4rbtv\.com$
(^|\.)4shared\.com$
(^|\.)4tern\.com$
(^|\.)51\.ca$
(^|\.)51jav\.org$
(^|\.)51luoben\.com$
(^|\.)5278\.cc$
(^|\.)5299\.tv$
(^|\.)56cun04\.jigsy\.com$
(^|\.)5aimiku\.com$
(^|\.)5i01\.com$
......@@ -145,10 +148,13 @@
(^|\.)a5\.com\.ru$
(^|\.)aamacau\.com$
(^|\.)abc\.com$
(^|\.)abc\.net\.au$
(^|\.)abc\.pp\.ru$
(^|\.)abc\.xyz$
(^|\.)abchinese\.com$
(^|\.)abclite\.net$
(^|\.)abebooks\.com$
(^|\.)abematv\.akamaized\.net$
(^|\.)abitno\.linpie\.com$
(^|\.)ablwang\.com$
(^|\.)aboluowang\.com$
......@@ -167,10 +173,11 @@
(^|\.)actimes\.com\.au$
(^|\.)activpn\.com$
(^|\.)aculo\.us$
(^|\.)adcex\.com$
(^|\.)addictedtocoffee\.de$
(^|\.)adelaidebbs\.com$
(^|\.)admin\.recaptcha\.net$
(^|\.)admob\.com$
(^|\.)adorama\.com$
(^|\.)adpl\.org\.hk$
(^|\.)ads-twitter\.com$
(^|\.)adsense\.com$
......@@ -183,6 +190,7 @@
(^|\.)ae\.hao123\.com$
(^|\.)ae\.org$
(^|\.)aenhancers\.com$
(^|\.)aex\.com$
(^|\.)af\.mil$
(^|\.)afantibbs\.com$
(^|\.)agnesb\.fr$
......@@ -192,6 +200,7 @@
(^|\.)ai-kan\.net$
(^|\.)ai-wen\.net$
(^|\.)ai\.binwang\.me$
(^|\.)ai\.google$
(^|\.)aiph\.net$
(^|\.)airasia\.com$
(^|\.)airconsole\.com$
......@@ -203,6 +212,7 @@
(^|\.)akademiye\.org$
(^|\.)akiba-online\.com$
(^|\.)akiba-web\.com$
(^|\.)akow\.org$
(^|\.)al-islam\.com$
(^|\.)al-qimmah\.net$
(^|\.)alabout\.com$
......@@ -213,9 +223,9 @@
(^|\.)alforattv\.net$
(^|\.)alhayat\.com$
(^|\.)alicejapan\.co\.jp$
(^|\.)alien-ufos\.com$
(^|\.)aliengu\.com$
(^|\.)alkasir\.com$
(^|\.)allcoin\.com$
(^|\.)allconnected\.co$
(^|\.)alldrawnsex\.com$
(^|\.)allervpn\.com$
......@@ -239,6 +249,7 @@
(^|\.)alwaysdata\.net$
(^|\.)alwaysvpn\.com$
(^|\.)am730\.com\.hk$
(^|\.)amazon\.co\.jp$
(^|\.)amazon\.com$
(^|\.)ameblo\.jp$
(^|\.)americangreencard\.com$
......@@ -261,8 +272,11 @@
(^|\.)android\.com$
(^|\.)androidify\.com$
(^|\.)androidplus\.co$
(^|\.)androidtv\.com$
(^|\.)andygod\.com$
(^|\.)angela-merkel\.de$
(^|\.)angelfire\.com$
(^|\.)angola\.org$
(^|\.)angularjs\.org$
(^|\.)animecrazy\.net$
(^|\.)animeshippuuden\.com$
......@@ -287,6 +301,8 @@
(^|\.)aojiao\.org$
(^|\.)aolchannels\.aol\.com$
(^|\.)aomiwang\.com$
(^|\.)apartmentratings\.com$
(^|\.)apartments\.com$
(^|\.)apetube\.com$
(^|\.)api-secure\.recaptcha\.net$
(^|\.)api-verify\.recaptcha\.net$
......@@ -294,6 +310,7 @@
(^|\.)api\.dropboxapi\.com$
(^|\.)api\.linksalpha\.com$
(^|\.)api\.proxlet\.com$
(^|\.)api\.pureapk\.com$
(^|\.)api\.recaptcha\.net$
(^|\.)apiary\.io$
(^|\.)apidocs\.linksalpha\.com$
......@@ -307,17 +324,23 @@
(^|\.)aplusvpn\.com$
(^|\.)app\.box\.com$
(^|\.)app\.heywire\.com$
(^|\.)app\.smartmailcloud\.com$
(^|\.)app\.tutanota\.com$
(^|\.)appdownloader\.net$
(^|\.)appledaily\.com$
(^|\.)appledaily\.com\.hk$
(^|\.)appledaily\.com\.tw$
(^|\.)appshopper\.com$
(^|\.)appsocks\.net$
(^|\.)appspot\.com$
(^|\.)appsto\.re$
(^|\.)aptoide\.com$
(^|\.)ar\.hao123\.com$
(^|\.)archive\.fo$
(^|\.)archive\.is$
(^|\.)archive\.li$
(^|\.)archive\.org$
(^|\.)archive\.today$
(^|\.)archives\.gov$
(^|\.)archives\.gov\.tw$
(^|\.)arctosia\.com$
......@@ -327,10 +350,10 @@
(^|\.)arlingtoncemetery\.mil$
(^|\.)army\.mil$
(^|\.)art4tibet1998\.org$
(^|\.)arte\.tv$
(^|\.)artofpeacefoundation\.org$
(^|\.)artsy\.net$
(^|\.)asacp\.org$
(^|\.)asahichinese\.com$
(^|\.)asdfg\.jp$
(^|\.)asg\.to$
(^|\.)asia-gaming\.com$
......@@ -344,6 +367,7 @@
(^|\.)askstudent\.com$
(^|\.)askynz\.net$
(^|\.)assembla\.com$
(^|\.)assets\.bwbx\.io$
(^|\.)assimp\.org$
(^|\.)astrill\.com$
(^|\.)atc\.org\.au$
......@@ -358,6 +382,7 @@
(^|\.)authorizeddns\.org$
(^|\.)authorizeddns\.us$
(^|\.)autodraw\.com$
(^|\.)av-e-body\.com$
(^|\.)av\.com$
(^|\.)av\.movie$
(^|\.)av\.nightlife141\.com$
......@@ -368,6 +393,7 @@
(^|\.)avdb\.in$
(^|\.)avdb\.tv$
(^|\.)avfantasy\.com$
(^|\.)avgle\.com$
(^|\.)avidemux\.org$
(^|\.)avmo\.pw$
(^|\.)avmoo\.com$
......@@ -384,17 +410,22 @@
(^|\.)backchina\.com$
(^|\.)backpackers\.com\.tw$
(^|\.)backtotiananmen\.com$
(^|\.)badiucao\.com$
(^|\.)badjojo\.com$
(^|\.)badoo\.com$
(^|\.)baidu\.jp$
(^|\.)baijie\.org$
(^|\.)bailandaily\.com$
(^|\.)baixing\.me$
(^|\.)bakgeekhome\.tk$
(^|\.)banana-vpn\.com$
(^|\.)band\.us$
(^|\.)bandwagonhost\.com$
(^|\.)bangbrosnetwork\.com$
(^|\.)bangchen\.net$
(^|\.)bangdream\.space$
(^|\.)bangyoulater\.com$
(^|\.)bankmobilevibe\.com$
(^|\.)bannedbook\.org$
(^|\.)bannednews\.org$
(^|\.)banorte\.com$
......@@ -409,10 +440,10 @@
(^|\.)bb\.ttv\.com\.tw$
(^|\.)bbc\.co\.uk$
(^|\.)bbc\.com$
(^|\.)bbc\.com%2fzhongwen$
(^|\.)bbc\.in$
(^|\.)bbcchinese\.com$
(^|\.)bbchat\.tv$
(^|\.)bbci\.co\.uk$
(^|\.)bbg\.gov$
(^|\.)bbkz\.com$
(^|\.)bbnradio\.org$
......@@ -425,6 +456,7 @@
(^|\.)bbs\.huasing\.org$
(^|\.)bbs\.junglobal\.net$
(^|\.)bbs\.kimy\.com\.tw$
(^|\.)bbs\.mikocon\.com$
(^|\.)bbs\.morbell\.com$
(^|\.)bbs\.mychat\.to$
(^|\.)bbs\.netbig\.com$
......@@ -444,12 +476,14 @@
(^|\.)bcast\.co\.nz$
(^|\.)bcc\.com\.tw$
(^|\.)bcchinese\.net$
(^|\.)bcex\.ca$
(^|\.)bcmorning\.com$
(^|\.)bdsmvideos\.net$
(^|\.)beaconevents\.com$
(^|\.)bebo\.com$
(^|\.)beeg\.com$
(^|\.)beevpn\.com$
(^|\.)behance\.net$
(^|\.)behindkink\.com$
(^|\.)beijing1989\.com$
(^|\.)beijingspring\.com$
......@@ -469,7 +503,6 @@
(^|\.)bestvpnservice\.com$
(^|\.)bestvpnusa\.com$
(^|\.)bet365\.com$
(^|\.)beta\.usejump\.com$
(^|\.)betfair\.com$
(^|\.)betternet\.co$
(^|\.)bettervpn\.com$
......@@ -484,7 +517,9 @@
(^|\.)biantailajiao\.com$
(^|\.)biantailajiao\.in$
(^|\.)biblesforamerica\.org$
(^|\.)bibox\.com$
(^|\.)bic2011\.org$
(^|\.)big\.one$
(^|\.)bigfools\.com$
(^|\.)bigjapanesesex\.com$
(^|\.)bigmoney\.biz$
......@@ -492,12 +527,19 @@
(^|\.)bigsound\.org$
(^|\.)biliworld\.com$
(^|\.)billypan\.com$
(^|\.)binance\.com$
(^|\.)binux\.me$
(^|\.)bipic\.net$
(^|\.)bird\.so$
(^|\.)bit-z\.com$
(^|\.)bit\.do$
(^|\.)bit\.ly$
(^|\.)bitc\.bme\.emory\.edu$
(^|\.)bitcointalk\.org$
(^|\.)bitcoinworld\.com$
(^|\.)bitfinex\.com$
(^|\.)bithumb\.com$
(^|\.)bitinka\.com\.ar$
(^|\.)bitmex\.com$
(^|\.)bitshare\.com$
(^|\.)bitsnoop\.com$
......@@ -560,7 +602,10 @@
(^|\.)blogs\.libraryinformationtechnology\.com$
(^|\.)blogs\.tampabay\.com$
(^|\.)blogs\.yahoo\.co\.jp$
(^|\.)blogspot(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}$
(^|\.)blogspot\.com$
(^|\.)blogspot\.hk$
(^|\.)blogspot\.jp$
(^|\.)blogspot\.tw$
(^|\.)blogtd\.net$
(^|\.)blogtd\.org$
(^|\.)bloodshed\.net$
......@@ -572,6 +617,7 @@
(^|\.)blueangellive\.com$
(^|\.)bmfinn\.com$
(^|\.)bnews\.co$
(^|\.)bnn\.co$
(^|\.)bnrmetal\.com$
(^|\.)boardreader\.com$
(^|\.)bod\.asia$
......@@ -587,6 +633,7 @@
(^|\.)book\.zi5\.me$
(^|\.)bookepub\.com$
(^|\.)books\.com\.tw$
(^|\.)booktopia\.com\.au$
(^|\.)boomssr\.com$
(^|\.)bot\.nu$
(^|\.)botanwang\.com$
......@@ -625,10 +672,14 @@
(^|\.)bt95\.com$
(^|\.)btaia\.com$
(^|\.)btbtav\.com$
(^|\.)btc98\.com$
(^|\.)btcbank\.bank$
(^|\.)btctrade\.im$
(^|\.)btdigg\.org$
(^|\.)btku\.me$
(^|\.)btku\.org$
(^|\.)btspread\.com$
(^|\.)btsynckeys\.com$
(^|\.)budaedu\.org$
(^|\.)buddhanet\.com\.tw$
(^|\.)buddhistchannel\.tv$
......@@ -647,13 +698,17 @@
(^|\.)buzzhand\.net$
(^|\.)buzzorange\.com$
(^|\.)bvpn\.com$
(^|\.)bwgyhw\.com$
(^|\.)bwh1\.net$
(^|\.)bwsj\.hk$
(^|\.)bx\.in\.th$
(^|\.)bx\.tl$
(^|\.)bynet\.co\.il$
(^|\.)c-est-simple\.com$
(^|\.)c-spanvideo\.org$
(^|\.)c100tibet\.org$
(^|\.)c1522\.mooo\.com$
(^|\.)c2cx\.com$
(^|\.)cablegatesearch\.net$
(^|\.)cachinese\.com$
(^|\.)cacnw\.com$
......@@ -693,6 +748,7 @@
(^|\.)casinoriva\.com$
(^|\.)castbox\.fm$
(^|\.)catch22\.net$
(^|\.)catchgod\.com$
(^|\.)catfightpayperview\.xxx$
(^|\.)catholic\.org\.hk$
(^|\.)catholic\.org\.tw$
......@@ -711,6 +767,7 @@
(^|\.)cclife\.org$
(^|\.)cclifefl\.org$
(^|\.)ccthere\.com$
(^|\.)ccthere\.net$
(^|\.)cctmweb\.net$
(^|\.)cctongbao\.com$
(^|\.)ccue\.ca$
......@@ -728,6 +785,7 @@
(^|\.)cdn\.assets\.lfpcontent\.com$
(^|\.)cdn\.helixstudios\.net$
(^|\.)cdn\.printfriendly\.com$
(^|\.)cdn\.seatguru\.com$
(^|\.)cdn\.softlayer\.net$
(^|\.)cdn1\.lp\.saboom\.com$
(^|\.)cdnews\.com\.tw$
......@@ -751,6 +809,7 @@
(^|\.)certificate-transparency\.org$
(^|\.)certificate\.revocationcheck\.com$
(^|\.)cfhks\.org\.hk$
(^|\.)cfos\.de$
(^|\.)cftfc\.com$
(^|\.)cgdepot\.org$
(^|\.)cgst\.edu$
......@@ -762,6 +821,7 @@
(^|\.)changp\.com$
(^|\.)changsa\.net$
(^|\.)channel8news\.sg$
(^|\.)chaoex\.com$
(^|\.)chapm25\.com$
(^|\.)chatnook\.com$
(^|\.)chaturbate\.com$
......@@ -824,6 +884,7 @@
(^|\.)chinatopsex\.com$
(^|\.)chinatown\.com\.au$
(^|\.)chinatweeps\.com$
(^|\.)chinaview\.wordpress\.com$
(^|\.)chinaway\.org$
(^|\.)chinaworker\.info$
(^|\.)chinaxchina\.com$
......@@ -841,6 +902,7 @@
(^|\.)chinesedemocracy\.com$
(^|\.)chinesegay\.org$
(^|\.)chinesen\.de$
(^|\.)chinesenews\.net\.au$
(^|\.)chinesepen\.org$
(^|\.)chinesetalks\.net$
(^|\.)chineseupress\.com$
......@@ -848,6 +910,7 @@
(^|\.)chinman\.net$
(^|\.)chithu\.org$
(^|\.)chn\.chosun\.com$
(^|\.)chobit\.cc$
(^|\.)chrdnet\.com$
(^|\.)christianfreedom\.org$
(^|\.)christianstudy\.com$
......@@ -870,6 +933,7 @@
(^|\.)cineastentreff\.de$
(^|\.)cipfg\.org$
(^|\.)circlethebayfortibet\.org$
(^|\.)cirosantilli\.com$
(^|\.)citizencn\.com$
(^|\.)citizenlab\.org$
(^|\.)citizenscommission\.hk$
......@@ -893,19 +957,24 @@
(^|\.)cleansite\.info$
(^|\.)cleansite\.us$
(^|\.)clearharmony\.net$
(^|\.)clearsurance\.com$
(^|\.)clearwisdom\.net$
(^|\.)clementine-player\.org$
(^|\.)cling\.omy\.sg$
(^|\.)clinica-tibet\.ru$
(^|\.)clipfish\.de$
(^|\.)cloakpoint\.com$
(^|\.)cloud\.feedly\.com$
(^|\.)cloud\.mail\.ru$
(^|\.)club1069\.com$
(^|\.)clyp\.it$
(^|\.)cmcn\.org$
(^|\.)cmi\.org\.tw$
(^|\.)cmp\.hku\.hk$
(^|\.)cms\.gov$
(^|\.)cmule\.com$
(^|\.)cmule\.org$
(^|\.)cmx\.im$
(^|\.)cn-proxy\.com$
(^|\.)cn\.calameo\.com$
(^|\.)cn\.dayabook\.com$
......@@ -917,6 +986,7 @@
(^|\.)cn\.sandscotaicentral\.com$
(^|\.)cn\.shafaqna\.com$
(^|\.)cn\.streetvoice\.com$
(^|\.)cn\.thegay\.com$
(^|\.)cn\.uncyclopedia\.wikia\.com$
(^|\.)cn\.uptodown\.com$
(^|\.)cn\.voa\.mobi$
......@@ -934,11 +1004,21 @@
(^|\.)cnproxy\.com$
(^|\.)co\.ng\.mil$
(^|\.)coat\.co\.jp$
(^|\.)cobinhood\.com$
(^|\.)cochina\.co$
(^|\.)cochina\.org$
(^|\.)code1984\.com$
(^|\.)codeshare\.io$
(^|\.)codeskulptor\.org$
(^|\.)coin2co\.in$
(^|\.)coinbene\.com$
(^|\.)coinegg\.com$
(^|\.)coinex\.com$
(^|\.)coingi\.com$
(^|\.)coinrail\.co\.kr$
(^|\.)cointiger\.com$
(^|\.)cointobe\.com$
(^|\.)coinut\.com$
(^|\.)collateralmurder\.com$
(^|\.)collateralmurder\.org$
(^|\.)com\.google$
......@@ -961,21 +1041,27 @@
(^|\.)coolder\.com$
(^|\.)coolloud\.org\.tw$
(^|\.)coolncute\.com$
(^|\.)coolstuffinc\.com$
(^|\.)corumcollege\.com$
(^|\.)cos-moe\.com$
(^|\.)cosmic\.monar\.ch$
(^|\.)cosplayjav\.pl$
(^|\.)costco\.com$
(^|\.)cotweet\.com$
(^|\.)counter\.social$
(^|\.)coursehero\.com$
(^|\.)cpj\.org$
(^|\.)cq99\.us$
(^|\.)crackle\.com$
(^|\.)crazys\.cc$
(^|\.)crazyshit\.com$
(^|\.)crbug\.com$
(^|\.)crchina\.org$
(^|\.)crd-net\.org$
(^|\.)creaders\.net$
(^|\.)creadersnet\.com$
(^|\.)creativelab5\.com$
(^|\.)crisisresponse\.google$
(^|\.)cristyli\.com$
(^|\.)crocotube\.com$
(^|\.)crossfire\.co\.kr$
......@@ -1000,6 +1086,7 @@
(^|\.)cumlouder\.com$
(^|\.)curvefish\.com$
(^|\.)cusu\.hk$
(^|\.)cutscenes\.net$
(^|\.)cw\.com\.tw$
(^|\.)cyberghost\.natado\.com$
(^|\.)cyberghostvpn\.com$
......@@ -1019,15 +1106,18 @@
(^|\.)dabr\.mobi$
(^|\.)dadazim\.com$
(^|\.)dadi360\.com$
(^|\.)dafabet\.com$
(^|\.)dafagood\.com$
(^|\.)dafahao\.com$
(^|\.)dafoh\.org$
(^|\.)daftporn\.com$
(^|\.)dagelijksestandaard\.nl$
(^|\.)daidostup\.ru$
(^|\.)dailidaili\.com$
(^|\.)dailymotion\.com$
(^|\.)dailynews\.sina\.com$
(^|\.)dailynews\.sina\.com%2f$
(^|\.)dailyview\.tw$
(^|\.)daiphapinfo\.net$
(^|\.)dajiyuan\.com$
(^|\.)dajiyuan\.de$
......@@ -1115,6 +1205,7 @@
(^|\.)dhcp\.biz$
(^|\.)diaoyuislands\.org$
(^|\.)dictionary\.goo\.ne\.jp$
(^|\.)difangwenge\.org$
(^|\.)digiland\.tw$
(^|\.)digisfera\.com$
(^|\.)digitalnomadsproject\.org$
......@@ -1123,6 +1214,9 @@
(^|\.)dingchin\.com\.tw$
(^|\.)dipity\.com$
(^|\.)directcreative\.com$
(^|\.)discoins\.com$
(^|\.)discordapp\.com$
(^|\.)discordapp\.net$
(^|\.)discuss\.com\.hk$
(^|\.)discuss4u\.com$
(^|\.)dish\.com$
......@@ -1173,6 +1267,8 @@
(^|\.)dotsub\.com$
(^|\.)dotvpn\.com$
(^|\.)doub\.io$
(^|\.)doubibackup\.com$
(^|\.)doubmirror\.cf$
(^|\.)dougscripts\.com$
(^|\.)douhokanko\.net$
(^|\.)doujincafe\.com$
......@@ -1183,6 +1279,7 @@
(^|\.)dphk\.org$
(^|\.)dpp\.org\.tw$
(^|\.)dpr\.info$
(^|\.)dragonex\.io$
(^|\.)dragonsprings\.org$
(^|\.)dreamamateurs\.com$
(^|\.)drepung\.org$
......@@ -1218,6 +1315,7 @@
(^|\.)dupola\.com$
(^|\.)dupola\.net$
(^|\.)dushi\.ca$
(^|\.)dvdpac\.com$
(^|\.)dvorak\.org$
(^|\.)dw-world\.com$
(^|\.)dw-world\.de$
......@@ -1239,6 +1337,7 @@
(^|\.)dynu\.com$
(^|\.)dynu\.net$
(^|\.)dynupdate\.no-ip\.com$
(^|\.)dysfz\.cc$
(^|\.)dzze\.com$
(^|\.)e-classical\.com\.tw$
(^|\.)e-gold\.com$
......@@ -1263,6 +1362,7 @@
(^|\.)ebook\.hyread\.com\.tw$
(^|\.)ebookbrowse\.com$
(^|\.)ebookee\.com$
(^|\.)ebtcbank\.com$
(^|\.)ecfa\.org\.tw$
(^|\.)echofon\.com$
(^|\.)ecimg\.tw$
......@@ -1290,6 +1390,7 @@
(^|\.)eksisozluk\.com$
(^|\.)electionsmeter\.com$
(^|\.)elgoog\.im$
(^|\.)ellawine\.org$
(^|\.)elpais\.com$
(^|\.)eltondisney\.com$
(^|\.)emaga\.com$
......@@ -1353,9 +1454,11 @@
(^|\.)eroticsaloon\.net$
(^|\.)eslite\.com$
(^|\.)esmtp\.biz$
(^|\.)esurance\.com$
(^|\.)etaa\.org\.au$
(^|\.)etadult\.com$
(^|\.)etaiwannews\.com$
(^|\.)etherdelta\.com$
(^|\.)etizer\.org$
(^|\.)etokki\.com$
(^|\.)etools\.ncol\.com$
......@@ -1367,10 +1470,12 @@
(^|\.)eucasino\.com$
(^|\.)eulam\.com$
(^|\.)eurekavpt\.com$
(^|\.)euronews\.com$
(^|\.)evchk\.wikia\.com$
(^|\.)evschool\.net$
(^|\.)exblog\.jp$
(^|\.)exchristian\.hk$
(^|\.)exmo\.com$
(^|\.)exmormon\.org$
(^|\.)expatshield\.com$
(^|\.)expecthim\.com$
......@@ -1378,8 +1483,10 @@
(^|\.)experts-univers\.com$
(^|\.)exploader\.net$
(^|\.)expressvpn\.com$
(^|\.)exrates\.me$
(^|\.)extmatrix\.com$
(^|\.)extremetube\.com$
(^|\.)exx\.com$
(^|\.)eyevio\.jp$
(^|\.)eyny\.com$
(^|\.)ezpc\.tk$
......@@ -1393,6 +1500,7 @@
(^|\.)facebook\.in$
(^|\.)facebook\.nl$
(^|\.)facebook\.se$
(^|\.)facebookmail\.com$
(^|\.)facebookquotes4u\.com$
(^|\.)faceless\.me$
(^|\.)facesofnyfw\.com$
......@@ -1453,13 +1561,16 @@
(^|\.)fastpic\.ru$
(^|\.)fastssh\.com$
(^|\.)faststone\.org$
(^|\.)fatbtc\.com$
(^|\.)favstar\.fm$
(^|\.)fawanghuihui\.org$
(^|\.)faydao\.com$
(^|\.)fb\.com$
(^|\.)fb\.me$
(^|\.)fbaddins\.com$
(^|\.)fbcdn\.net$
(^|\.)fbsbx\.com$
(^|\.)fbworkmail\.com$
(^|\.)fc2\.com$
(^|\.)fc2blog\.net$
(^|\.)fc2china\.com$
......@@ -1499,6 +1610,7 @@
(^|\.)finchvpn\.com$
(^|\.)findmespot\.com$
(^|\.)findyoutube\.com$
(^|\.)findyoutube\.net$
(^|\.)fingerdaily\.com$
(^|\.)finler\.net$
(^|\.)firearmsworld\.net$
......@@ -1521,6 +1633,10 @@
(^|\.)flitto\.com$
(^|\.)flnet\.org$
(^|\.)flog\.tw$
(^|\.)flyvpn\.com$
(^|\.)flyzy2005\.com$
(^|\.)fnac\.be$
(^|\.)fnac\.com$
(^|\.)fochk\.org$
(^|\.)focustaiwan\.tw$
(^|\.)focusvpn\.com$
......@@ -1529,6 +1645,7 @@
(^|\.)fofldfradio\.org$
(^|\.)fooooo\.com$
(^|\.)footwiball\.com$
(^|\.)foreignpolicy\.com$
(^|\.)forum\.baby-kingdom\.com$
(^|\.)forum\.cyberctm\.com$
(^|\.)forum\.idsam\.com$
......@@ -1561,6 +1678,7 @@
(^|\.)free-gate\.org$
(^|\.)free-hada-now\.org$
(^|\.)free-proxy\.cz$
(^|\.)free-ss\.site$
(^|\.)free-ssh\.com$
(^|\.)free\.fr$
(^|\.)free4u\.com\.ar$
......@@ -1570,6 +1688,7 @@
(^|\.)freechina\.net$
(^|\.)freechina\.news$
(^|\.)freechinaforum\.org$
(^|\.)freechinaweibo\.com$
(^|\.)freeddns\.com$
(^|\.)freeddns\.org$
(^|\.)freedomchina\.info$
......@@ -1657,6 +1776,7 @@
(^|\.)g-area\.org$
(^|\.)g-queen\.com$
(^|\.)g\.co$
(^|\.)g0v\.social$
(^|\.)g6hentai\.com$
(^|\.)gabocorp\.com$
(^|\.)gaeproxy\.com$
......@@ -1667,7 +1787,9 @@
(^|\.)game735\.com$
(^|\.)gamebase\.com\.tw$
(^|\.)gamejolt\.com$
(^|\.)gamer-cds\.cdn\.hinet\.net$
(^|\.)gamer\.com\.tw$
(^|\.)gamer2-cds\.cdn\.hinet\.net$
(^|\.)gamez\.com\.tw$
(^|\.)gamousa\.com$
(^|\.)ganges\.com$
......@@ -1679,6 +1801,8 @@
(^|\.)gardennetworks\.org$
(^|\.)gartlive\.com$
(^|\.)gate-project\.com$
(^|\.)gate\.io$
(^|\.)gatecoin\.com$
(^|\.)gather\.com$
(^|\.)gatherproxy\.com$
(^|\.)gati\.org\.tw$
......@@ -1686,11 +1810,13 @@
(^|\.)gaycn\.net$
(^|\.)gayhub\.com$
(^|\.)gaymap\.cc$
(^|\.)gaymenring\.com$
(^|\.)gaytube\.com$
(^|\.)gaywatch\.com$
(^|\.)gazotube\.com$
(^|\.)gcc\.org\.hk$
(^|\.)gclooney\.com$
(^|\.)gcmasia\.com$
(^|\.)gcpnews\.com$
(^|\.)gcr\.io$
(^|\.)gdbt\.net$
......@@ -1704,7 +1830,9 @@
(^|\.)geocities\.com$
(^|\.)geocities\.jp$
(^|\.)gerefoundation\.org$
(^|\.)get\.app$
(^|\.)get\.how$
(^|\.)get\.page$
(^|\.)getastrill\.com$
(^|\.)getchu\.com$
(^|\.)getcloak\.com$
......@@ -1716,6 +1844,7 @@
(^|\.)getjetso\.com$
(^|\.)getlantern\.org$
(^|\.)getmdl\.io$
(^|\.)getoutline\.org$
(^|\.)getsocialscope\.com$
(^|\.)getsync\.com$
(^|\.)gettrials\.com$
......@@ -1728,7 +1857,6 @@
(^|\.)gfw\.press$
(^|\.)ggpht\.com$
(^|\.)ggssl\.com$
(^|\.)ghost\.org$
(^|\.)ghostpath\.com$
(^|\.)ghut\.org$
(^|\.)giantessnight\.com$
......@@ -1737,6 +1865,7 @@
(^|\.)gigporno\.ru$
(^|\.)girlbanker\.com$
(^|\.)git\.io$
(^|\.)github\.com$
(^|\.)gizlen\.net$
(^|\.)gjczz\.com$
(^|\.)glass8\.eu$
......@@ -1799,8 +1928,61 @@
(^|\.)goodtv\.com\.tw$
(^|\.)goodtv\.tv$
(^|\.)goofind\.com$
(^|\.)google(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}$
(^|\.)googleapis(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}$
(^|\.)google\.ae$
(^|\.)google\.am$
(^|\.)google\.as$
(^|\.)google\.at$
(^|\.)google\.az$
(^|\.)google\.ba$
(^|\.)google\.be$
(^|\.)google\.bg$
(^|\.)google\.ca$
(^|\.)google\.calstate\.edu$
(^|\.)google\.cd$
(^|\.)google\.ci$
(^|\.)google\.co\.id$
(^|\.)google\.co\.jp$
(^|\.)google\.co\.kr$
(^|\.)google\.co\.ma$
(^|\.)google\.co\.uk$
(^|\.)google\.com$
(^|\.)google\.de$
(^|\.)google\.dj$
(^|\.)google\.dk$
(^|\.)google\.es$
(^|\.)google\.fi$
(^|\.)google\.fm$
(^|\.)google\.fr$
(^|\.)google\.gg$
(^|\.)google\.gl$
(^|\.)google\.gr$
(^|\.)google\.ie$
(^|\.)google\.is$
(^|\.)google\.it$
(^|\.)google\.jo$
(^|\.)google\.kz$
(^|\.)google\.lv$
(^|\.)google\.mn$
(^|\.)google\.ms$
(^|\.)google\.nl$
(^|\.)google\.no$
(^|\.)google\.nu$
(^|\.)google\.ro$
(^|\.)google\.ru$
(^|\.)google\.rw$
(^|\.)google\.sc$
(^|\.)google\.sh$
(^|\.)google\.sk$
(^|\.)google\.sm$
(^|\.)google\.sn$
(^|\.)google\.tk$
(^|\.)google\.tm$
(^|\.)google\.to$
(^|\.)google\.tt$
(^|\.)google\.vu$
(^|\.)google\.ws$
(^|\.)googleapis\.cn$
(^|\.)googleapis\.com$
(^|\.)googleapps\.com$
(^|\.)googlearth\.com$
(^|\.)googleartproject\.com$
......@@ -1831,6 +2013,7 @@
(^|\.)googlezip\.net$
(^|\.)gopetition\.com$
(^|\.)goproxing\.net$
(^|\.)goregrish\.com$
(^|\.)gospelherald\.com$
(^|\.)got-game\.org$
(^|\.)gotdns\.ch$
......@@ -1864,8 +2047,11 @@
(^|\.)greenreadings\.com$
(^|\.)greenvpn\.net$
(^|\.)greenvpn\.org$
(^|\.)grotty-monday\.com$
(^|\.)groups\.google\.cn$
(^|\.)grow\.google$
(^|\.)gs-discuss\.com$
(^|\.)gsp\.target\.com$
(^|\.)gstatic\.com$
(^|\.)gtricks\.com$
(^|\.)gts-vpn\.com$
......@@ -1873,6 +2059,7 @@
(^|\.)guaguass\.com$
(^|\.)guaguass\.org$
(^|\.)guancha\.org$
(^|\.)guaneryu\.com$
(^|\.)guangming\.com\.my$
(^|\.)guardster\.com$
(^|\.)guishan\.org$
......@@ -1907,6 +2094,7 @@
(^|\.)hacken\.cc$
(^|\.)hacker\.org$
(^|\.)hackthatphone\.net$
(^|\.)hahaxixi\.github\.io$
(^|\.)hahlo\.com$
(^|\.)hakkatv\.org\.tw$
(^|\.)handcraftedsoftware\.org$
......@@ -1920,12 +2108,15 @@
(^|\.)hautelook\.com$
(^|\.)hautelookcdn\.com$
(^|\.)have8\.com$
(^|\.)hbg\.com$
(^|\.)hbo\.com$
(^|\.)hclips\.com$
(^|\.)hd\.stheadline\.com$
(^|\.)hdlt\.me$
(^|\.)hdtvb\.net$
(^|\.)hdzog\.com$
(^|\.)heartyit\.com$
(^|\.)heavy-r\.com$
(^|\.)hec\.su$
(^|\.)hecaitou\.net$
(^|\.)hechaji\.com$
......@@ -1977,6 +2168,7 @@
(^|\.)himalayanglacier\.com$
(^|\.)himemix\.com$
(^|\.)himemix\.net$
(^|\.)hitbtc\.com$
(^|\.)hitomi\.la$
(^|\.)hiwifi\.com$
(^|\.)hizb-ut-tahrir\.info$
......@@ -2020,6 +2212,7 @@
(^|\.)hkfaa\.com$
(^|\.)hkfreezone\.com$
(^|\.)hkfront\.org$
(^|\.)hkgalden\.com$
(^|\.)hkgolden\.com$
(^|\.)hkgreenradio\.org$
(^|\.)hkheadline\.com$
......@@ -2040,6 +2233,7 @@
(^|\.)hkwcc\.org\.hk$
(^|\.)hkzone\.org$
(^|\.)hmonghot\.com$
(^|\.)hmv\.co\.jp$
(^|\.)hmvdigital\.ca$
(^|\.)hmvdigital\.com$
(^|\.)hnjhj\.com$
......@@ -2073,6 +2267,7 @@
(^|\.)hotvpn\.com$
(^|\.)hougaige\.com$
(^|\.)howtoforge\.com$
(^|\.)hoxx\.com$
(^|\.)hpa\.gov\.tw$
(^|\.)hqcdp\.org$
(^|\.)hqjapanesesex\.com$
......@@ -2083,6 +2278,7 @@
(^|\.)hrcir\.com$
(^|\.)hrea\.org$
(^|\.)hrichina\.org$
(^|\.)hrtsea\.com$
(^|\.)hrw\.org$
(^|\.)hrweb\.org$
(^|\.)hsjp\.net$
......@@ -2109,6 +2305,7 @@
(^|\.)huaxin\.ph$
(^|\.)huayuworld\.org$
(^|\.)hudatoriq\.web\.id$
(^|\.)hudson\.org$
(^|\.)huffingtonpost\.com$
(^|\.)hugoroy\.eu$
(^|\.)huhaitai\.com$
......@@ -2193,6 +2390,7 @@
(^|\.)ihakka\.net$
(^|\.)ihao\.org$
(^|\.)iicns\.com$
(^|\.)iipdigital\.usembassy\.gov$
(^|\.)ikstar\.com$
(^|\.)ikwb\.com$
(^|\.)illusionfactory\.com$
......@@ -2240,16 +2438,19 @@
(^|\.)instagram\.com$
(^|\.)instanthq\.com$
(^|\.)institut-tibetain\.org$
(^|\.)international-news\.newsmagazine\.asia$
(^|\.)internet\.org$
(^|\.)internetdefenseleague\.org$
(^|\.)internetfreedom\.org$
(^|\.)internetpopculture\.com$
(^|\.)inthenameofconfuciusmovie\.com$
(^|\.)investigating\.wordpress\.com$
(^|\.)inxian\.com$
(^|\.)iownyour\.biz$
(^|\.)iownyour\.org$
(^|\.)ipalter\.com$
(^|\.)ipfire\.org$
(^|\.)ipfs\.io$
(^|\.)iphone4hongkong\.com$
(^|\.)iphonehacks\.com$
(^|\.)iphonetaiwan\.org$
......@@ -2272,6 +2473,7 @@
(^|\.)is\.gd$
(^|\.)isaacmao\.com$
(^|\.)isasecret\.com$
(^|\.)isc\.sans\.edu$
(^|\.)isgreat\.org$
(^|\.)islahhaber\.net$
(^|\.)islam\.org\.hk$
......@@ -2322,6 +2524,7 @@
(^|\.)janwongphoto\.com$
(^|\.)japan-whores\.com$
(^|\.)japanfirst\.asianfreeforum\.com$
(^|\.)japantimes\.co\.jp$
(^|\.)jav\.com$
(^|\.)jav101\.com$
(^|\.)jav2be\.com$
......@@ -2347,6 +2550,7 @@
(^|\.)jdwsy\.com$
(^|\.)jeanyim\.com$
(^|\.)jetos\.com$
(^|\.)jex\.com$
(^|\.)jfqu36\.club$
(^|\.)jfqu37\.xyz$
(^|\.)jgoodies\.com$
......@@ -2365,6 +2569,8 @@
(^|\.)jingsim\.org$
(^|\.)jinpianwang\.com$
(^|\.)jinroukong\.com$
(^|\.)jintian\.net$
(^|\.)jinx\.com$
(^|\.)jitouch\.com$
(^|\.)jizzthis\.com$
(^|\.)jjgirls\.com$
......@@ -2376,6 +2582,7 @@
(^|\.)joachims\.org$
(^|\.)jobnewera\.wordpress\.com$
(^|\.)jobso\.tv$
(^|\.)joinmastodon\.org$
(^|\.)journalchretien\.net$
(^|\.)journalofdemocracy\.org$
(^|\.)joymiihub\.com$
......@@ -2383,6 +2590,7 @@
(^|\.)jp\.hao123\.com$
(^|\.)jpl\.nasa\.gov$
(^|\.)jpopforum\.net$
(^|\.)jtvnw\.net$
(^|\.)jubushoushen\.com$
(^|\.)juhuaren\.com$
(^|\.)jukujo-club\.com$
......@@ -2425,6 +2633,7 @@
(^|\.)karmapa-teachings\.org$
(^|\.)karmapa\.org$
(^|\.)kawaiikawaii\.jp$
(^|\.)kawase\.com$
(^|\.)kb\.monitorware\.com$
(^|\.)kba-tx\.org$
(^|\.)kcoolonline\.com$
......@@ -2437,6 +2646,7 @@
(^|\.)kenengba\.com$
(^|\.)keontech\.net$
(^|\.)kepard\.com$
(^|\.)kex\.com$
(^|\.)keycdn\.com$
(^|\.)khabdha\.org$
(^|\.)khatrimaza\.org$
......@@ -2472,7 +2682,9 @@
(^|\.)korenan2\.com$
(^|\.)ksdl\.org$
(^|\.)ksnews\.com\.tw$
(^|\.)kspcoin\.com$
(^|\.)ktzhk\.com$
(^|\.)kucoin\.com$
(^|\.)kui\.name$
(^|\.)kun\.im$
(^|\.)kurashsultan\.com$
......@@ -2508,6 +2720,7 @@
(^|\.)lastfm\.es$
(^|\.)latelinenews\.com$
(^|\.)latibet\.org$
(^|\.)lbank\.info$
(^|\.)ld\.hao123img\.com$
(^|\.)le-vpn\.com$
(^|\.)leafyvpn\.net$
......@@ -2536,6 +2749,7 @@
(^|\.)lhakar\.org$
(^|\.)lhasocialwork\.org$
(^|\.)liangyou\.net$
(^|\.)liangzhichuanmei\.com$
(^|\.)lianyue\.net$
(^|\.)liaowangxizang\.net$
(^|\.)liberal\.org\.hk$
......@@ -2545,12 +2759,14 @@
(^|\.)lifemiles\.com$
(^|\.)lighten\.org\.tw$
(^|\.)lightnovel\.cn$
(^|\.)lihkg\.com$
(^|\.)like\.com$
(^|\.)limiao\.net$
(^|\.)line-apps\.com$
(^|\.)line-scdn\.net$
(^|\.)line\.me$
(^|\.)line\.naver\.jp$
(^|\.)linear-abematv\.akamaized\.net$
(^|\.)linglingfa\.com$
(^|\.)lingvodics\.com$
(^|\.)link-o-rama\.com$
......@@ -2564,11 +2780,13 @@
(^|\.)listentoyoutube\.com$
(^|\.)listorious\.com$
(^|\.)lists\.w3\.org$
(^|\.)liu-xiaobo\.org$
(^|\.)liudejun\.com$
(^|\.)liuhanyu\.com$
(^|\.)liujianshu\.com$
(^|\.)liuxiaobo\.net$
(^|\.)liuxiaotong\.com$
(^|\.)livecoin\.net$
(^|\.)livedoor\.jp$
(^|\.)liveleak\.com$
(^|\.)livestation\.com$
......@@ -2582,10 +2800,12 @@
(^|\.)llss\.me$
(^|\.)load\.to$
(^|\.)lobsangwangyal\.com$
(^|\.)localbitcoins\.com$
(^|\.)localdomain\.ws$
(^|\.)localpresshk\.com$
(^|\.)lockestek\.com$
(^|\.)logbot\.net$
(^|\.)login\.target\.com$
(^|\.)logiqx\.com$
(^|\.)londonchinese\.ca$
(^|\.)longhair\.hk$
......@@ -2630,6 +2850,7 @@
(^|\.)mad-ar\.ch$
(^|\.)madewithcode\.com$
(^|\.)madonna-av\.com$
(^|\.)madrau\.com$
(^|\.)madthumbs\.com$
(^|\.)magazines\.sina\.com\.tw$
(^|\.)magic-net\.info$
......@@ -2664,6 +2885,9 @@
(^|\.)marxists\.org$
(^|\.)mash\.to$
(^|\.)maskedip\.com$
(^|\.)mastodon\.cloud$
(^|\.)mastodon\.host$
(^|\.)mastodon\.social$
(^|\.)matainja\.com$
(^|\.)material\.io$
(^|\.)mathable\.io$
......@@ -2671,6 +2895,7 @@
(^|\.)matome-plus\.com$
(^|\.)matome-plus\.net$
(^|\.)matsushimakaede\.com$
(^|\.)matters\.news$
(^|\.)mattwilcox\.net$
(^|\.)maturejp\.com$
(^|\.)maxing\.jp$
......@@ -2680,6 +2905,7 @@
(^|\.)mcfog\.com$
(^|\.)mcreasite\.com$
(^|\.)md-t\.org$
(^|\.)me\.me$
(^|\.)me\.youthwant\.com\.tw$
(^|\.)meansys\.com$
(^|\.)media\.nu\.nl$
......@@ -2698,14 +2924,15 @@
(^|\.)megavideo\.com$
(^|\.)megurineluka\.com$
(^|\.)meirixiaochao\.com$
(^|\.)melon-peach\.com$
(^|\.)meltoday\.com$
(^|\.)meme\.yahoo\.com$
(^|\.)memehk\.com$
(^|\.)memorybbs\.com$
(^|\.)memri\.org$
(^|\.)memrijttm\.org$
(^|\.)mercatox\.com$
(^|\.)mercyprophet\.org$
(^|\.)mergersandinquisitions\.org$
(^|\.)meridian-trust\.org$
(^|\.)meripet\.biz$
(^|\.)meripet\.com$
......@@ -2714,6 +2941,7 @@
(^|\.)mesotw\.com$
(^|\.)messenger\.com$
(^|\.)metacafe\.com$
(^|\.)metart\.com$
(^|\.)metarthunter\.com$
(^|\.)meteorshowersonline\.com$
(^|\.)metrohk\.com\.hk$
......@@ -2779,6 +3007,7 @@
(^|\.)mjlsh\.usc\.cuhk\.edu\.hk$
(^|\.)mk5000\.com$
(^|\.)mlcool\.com$
(^|\.)mlzs\.work$
(^|\.)mm-cg\.com$
(^|\.)mmaaxx\.com$
(^|\.)mmmca\.com$
......@@ -2791,9 +3020,13 @@
(^|\.)mobypicture\.com$
(^|\.)moeaic\.gov\.tw$
(^|\.)moeerolibrary\.com$
(^|\.)mofa\.gov\.tw$
(^|\.)mofaxiehui\.com$
(^|\.)mofos\.com$
(^|\.)mog\.com$
(^|\.)mohu\.club$
(^|\.)mohu\.ml$
(^|\.)mojim\.com$
(^|\.)mol\.gov\.tw$
(^|\.)molihua\.org$
(^|\.)mondex\.org$
......@@ -2803,10 +3036,12 @@
(^|\.)monster\.com$
(^|\.)moodyz\.com$
(^|\.)moonbbs\.com$
(^|\.)moonbingo\.com$
(^|\.)morningsun\.org$
(^|\.)moroneta\.com$
(^|\.)mos\.ru$
(^|\.)motherless\.com$
(^|\.)motiyun\.com$
(^|\.)motor4ik\.ru$
(^|\.)mousebreaker\.com$
(^|\.)movements\.org$
......@@ -2823,6 +3058,7 @@
(^|\.)mrface\.com$
(^|\.)mrslove\.com$
(^|\.)mrtweet\.com$
(^|\.)msa-it\.org$
(^|\.)msguancha\.com$
(^|\.)msha\.gov$
(^|\.)mswe1\.org$
......@@ -2957,7 +3193,6 @@
(^|\.)newipnow\.com$
(^|\.)newlandmagazine\.com\.au$
(^|\.)newnews\.ca$
(^|\.)news\.bbc\.co\.uk$
(^|\.)news\.cnyes\.com$
(^|\.)news\.hk\.msn\.com$
(^|\.)news\.hkpeanut\.com$
......@@ -2980,7 +3215,6 @@
(^|\.)newscn\.org$
(^|\.)newsdetox\.ca$
(^|\.)newsdh\.com$
(^|\.)newsforums\.bbc\.co\.uk$
(^|\.)newspeak\.cc$
(^|\.)newstamago\.com$
(^|\.)newstapa\.org$
......@@ -3017,6 +3251,7 @@
(^|\.)ninjaproxy\.ninja$
(^|\.)nintendium\.com$
(^|\.)ninth\.biz$
(^|\.)niu\.moe$
(^|\.)niusnews\.com$
(^|\.)njactb\.org$
(^|\.)njuice\.com$
......@@ -3026,6 +3261,7 @@
(^|\.)nobel\.se$
(^|\.)nobelprize\.org$
(^|\.)nobodycanstop\.us$
(^|\.)nofile\.io$
(^|\.)nokogiri\.org$
(^|\.)nokola\.com$
(^|\.)noodlevpn\.com$
......@@ -3034,8 +3270,10 @@
(^|\.)nordstromimage\.com$
(^|\.)nordstromrack\.com$
(^|\.)nordvpn\.com$
(^|\.)notify\.dropboxapi\.com$
(^|\.)nottinghampost\.com$
(^|\.)novelasia\.com$
(^|\.)now\.com$
(^|\.)now\.im$
(^|\.)nownews\.com$
(^|\.)nowtorrents\.com$
......@@ -3044,6 +3282,7 @@
(^|\.)npa\.gov\.tw$
(^|\.)npnt\.me$
(^|\.)nps\.gov$
(^|\.)npsboost\.com$
(^|\.)nradio\.me$
(^|\.)nrk\.no$
(^|\.)ns01\.biz$
......@@ -3085,7 +3324,6 @@
(^|\.)ny\.stgloballink\.com$
(^|\.)ny\.visiontimes\.com$
(^|\.)nyaa\.eu$
(^|\.)nyaa\.si$
(^|\.)nydus\.ca$
(^|\.)nylon-angel\.com$
(^|\.)nylonstockingsonline\.com$
......@@ -3110,6 +3348,7 @@
(^|\.)october-review\.org$
(^|\.)oculus\.com$
(^|\.)oculuscdn\.com$
(^|\.)oex\.com$
(^|\.)offbeatchina\.com$
(^|\.)officeoftibet\.com$
(^|\.)ofile\.org$
......@@ -3120,6 +3359,7 @@
(^|\.)oizoblog\.com$
(^|\.)ok\.ru$
(^|\.)okayfreedom\.com$
(^|\.)okex\.com$
(^|\.)okk\.tw$
(^|\.)old-cat\.net$
(^|\.)old\.honeynet\.org$
......@@ -3135,6 +3375,7 @@
(^|\.)onapp\.com$
(^|\.)onedrive\.live\.com$
(^|\.)onedumb\.com$
(^|\.)onejav\.com$
(^|\.)onion\.city$
(^|\.)online\.recoveryversion\.org$
(^|\.)onlinecha\.com$
......@@ -3158,9 +3399,11 @@
(^|\.)openid\.net$
(^|\.)openleaks\.org$
(^|\.)openvpn\.net$
(^|\.)openvpn\.org$
(^|\.)openwebster\.com$
(^|\.)openwrt\.org\.cn$
(^|\.)opml\.radiotime\.com$
(^|\.)opus-gaming\.com$
(^|\.)organcare\.org\.tw$
(^|\.)organharvestinvestigation\.net$
(^|\.)organiccrap\.com$
......@@ -3171,6 +3414,7 @@
(^|\.)orn\.jp$
(^|\.)orzistic\.org$
(^|\.)osfoora\.com$
(^|\.)otcbtc\.com$
(^|\.)otnd\.org$
(^|\.)otto\.de$
(^|\.)otzo\.com$
......@@ -3203,14 +3447,12 @@
(^|\.)paldengyal\.com$
(^|\.)paljorpublications\.com$
(^|\.)paltalk\.com$
(^|\.)panacom\.co\.jp$
(^|\.)panamapapers\.sueddeutsche\.de$
(^|\.)pandapow\.co$
(^|\.)pandapow\.net$
(^|\.)pandavpn-jp\.com$
(^|\.)pandora\.com$
(^|\.)pandora\.tv$
(^|\.)pangci\.net$
(^|\.)panluan\.net$
(^|\.)panoramio\.com$
(^|\.)pao-pao\.net$
......@@ -3223,6 +3465,7 @@
(^|\.)partypoker\.com$
(^|\.)passion\.com$
(^|\.)passiontimes\.hk$
(^|\.)paste\.ee$
(^|\.)pastebin\.com$
(^|\.)pastie\.org$
(^|\.)pbs\.org$
......@@ -3283,6 +3526,7 @@
(^|\.)picturesocial\.com$
(^|\.)pin-cong\.com$
(^|\.)pin6\.com$
(^|\.)pincong\.rocks$
(^|\.)ping\.fm$
(^|\.)pinimg\.com$
(^|\.)pinkrod\.com$
......@@ -3306,6 +3550,7 @@
(^|\.)pixiv\.net$
(^|\.)pixnet\.net$
(^|\.)pk\.com$
(^|\.)pki\.goog$
(^|\.)placemix\.com$
(^|\.)playboy\.com$
(^|\.)playboyplus\.com$
......@@ -3315,6 +3560,7 @@
(^|\.)plays\.com\.tw$
(^|\.)plm\.org\.hk$
(^|\.)plunder\.com$
(^|\.)plurk\.com$
(^|\.)plus28\.com$
(^|\.)plusbb\.com$
(^|\.)pmatehunter\.com$
......@@ -3322,9 +3568,11 @@
(^|\.)po2b\.com$
(^|\.)pobieramy\.top$
(^|\.)podictionary\.com$
(^|\.)pokerstars\.com$
(^|\.)pokerstars\.net$
(^|\.)politicalchina\.org$
(^|\.)politicalconsultation\.org$
(^|\.)politiscales\.net$
(^|\.)poloniex\.com$
(^|\.)polymer-project\.org$
(^|\.)polymerhk\.com$
......@@ -3366,9 +3614,11 @@
(^|\.)powerphoto\.org$
(^|\.)prayforchina\.net$
(^|\.)premeforwindows7\.com$
(^|\.)premproxy\.com$
(^|\.)presentationzen\.com$
(^|\.)presidentlee\.tw$
(^|\.)prestige-av\.com$
(^|\.)pride\.google$
(^|\.)prisoner-state-secret-journal-premier$
(^|\.)prisoneralert\.com$
(^|\.)pritunl\.com$
......@@ -3380,6 +3630,7 @@
(^|\.)privatevpn\.com$
(^|\.)procopytips\.com$
(^|\.)prosiben\.de$
(^|\.)protonvpn\.com$
(^|\.)provideocoalition\.com$
(^|\.)provpnaccounts\.com$
(^|\.)proxfree\.com$
......@@ -3427,6 +3678,7 @@
(^|\.)python\.com$
(^|\.)python\.com\.tw$
(^|\.)pythonhackers\.com$
(^|\.)pytorch\.org$
(^|\.)q%3dfreedom$
(^|\.)q%3dtriangle$
(^|\.)q=freedom$
......@@ -3454,6 +3706,7 @@
(^|\.)quitccp\.net$
(^|\.)quitccp\.org$
(^|\.)quora\.com$
(^|\.)quoracdn\.net$
(^|\.)quran\.com$
(^|\.)quranexplorer\.com$
(^|\.)qusi8\.net$
......@@ -3488,6 +3741,7 @@
(^|\.)rawgit\.com$
(^|\.)rawgithub\.com$
(^|\.)razyboard\.com$
(^|\.)rcam\.target\.com$
(^|\.)rcinet\.ca$
(^|\.)rconversation\.blogs\.com$
(^|\.)rd\.com$
......@@ -3510,6 +3764,11 @@
(^|\.)redballoonsolidarity\.org$
(^|\.)redchinacn\.net$
(^|\.)redchinacn\.org$
(^|\.)redd\.it$
(^|\.)reddit\.com$
(^|\.)redditlist\.com$
(^|\.)redditmedia\.com$
(^|\.)redditstatic\.com$
(^|\.)redhotlabs\.com$
(^|\.)redtube\.com$
(^|\.)referer\.us$
......@@ -3533,16 +3792,21 @@
(^|\.)revver\.com$
(^|\.)rfa\.org$
(^|\.)rfachina\.com$
(^|\.)rfalive1\.akacast\.akamaistream\.net$
(^|\.)rfamobile\.org$
(^|\.)rfaweb\.org$
(^|\.)rferl\.org$
(^|\.)rfi\.fr$
(^|\.)rfi\.my$
(^|\.)rg3\.github\.io$
(^|\.)rightbtc\.com$
(^|\.)rigpa\.org$
(^|\.)riku\.me$
(^|\.)rileyguide\.com$
(^|\.)ritouki\.jp$
(^|\.)ritter\.vg$
(^|\.)rixcloud\.com$
(^|\.)rixcloud\.us$
(^|\.)rlwlw\.com$
(^|\.)rmjdw\.com$
(^|\.)rmjdw132\.info$
......@@ -3557,6 +3821,7 @@
(^|\.)roodo\.com$
(^|\.)rosechina\.net$
(^|\.)rotten\.com$
(^|\.)rsdlmonitor\.com$
(^|\.)rsf-chinese\.org$
(^|\.)rsf\.org$
(^|\.)rsgamen\.org$
......@@ -3564,10 +3829,12 @@
(^|\.)rtalabel\.org$
(^|\.)rthk\.hk$
(^|\.)rthk\.org\.hk$
(^|\.)rthklive2-lh\.akamaihd\.net$
(^|\.)rti\.org\.tw$
(^|\.)rtycminnesota\.org$
(^|\.)ruanyifeng\.com$
(^|\.)rukor\.org$
(^|\.)runbtx\.com$
(^|\.)rushbee\.com$
(^|\.)ruten\.com\.tw$
(^|\.)rutube\.ru$
......@@ -3587,6 +3854,7 @@
(^|\.)sadistic-v\.com$
(^|\.)sadpanda\.us$
(^|\.)safervpn\.com$
(^|\.)safety\.google$
(^|\.)saintyculture\.com$
(^|\.)saiq\.me$
(^|\.)sakuralive\.com$
......@@ -3599,6 +3867,7 @@
(^|\.)saveliuxiaobo\.com$
(^|\.)savemedia\.com$
(^|\.)savethedate\.foo$
(^|\.)savethesounds\.info$
(^|\.)savetibet\.de$
(^|\.)savetibet\.fr$
(^|\.)savetibet\.nl$
......@@ -3609,6 +3878,9 @@
(^|\.)say2\.info$
(^|\.)sbme\.me$
(^|\.)sbs\.com\.au$
(^|\.)scache\.vzw\.com$
(^|\.)scache1\.vzw\.com$
(^|\.)scache2\.vzw\.com$
(^|\.)scasino\.com$
(^|\.)schema\.org$
(^|\.)sciencenets\.com$
......@@ -3622,6 +3894,7 @@
(^|\.)search$
(^|\.)search\.aol\.com$
(^|\.)search\.yahoo\.co\.jp$
(^|\.)search\.yahoo\.com$
(^|\.)searchtruth\.com$
(^|\.)secretchina\.com$
(^|\.)secretgarden\.no$
......@@ -3630,6 +3903,7 @@
(^|\.)secure\.logmein\.com$
(^|\.)secure\.raxcdn\.com$
(^|\.)securetunnel\.com$
(^|\.)securityinabox\.org$
(^|\.)securitykiss\.com$
(^|\.)seed4\.me$
(^|\.)seesmic\.com$
......@@ -3669,6 +3943,7 @@
(^|\.)shadowsky\.xyz$
(^|\.)shadowsocks-r\.com$
(^|\.)shadowsocks\.asia$
(^|\.)shadowsocks\.be$
(^|\.)shadowsocks\.com$
(^|\.)shadowsocks\.com\.hk$
(^|\.)shadowsocks\.org$
......@@ -3720,6 +3995,7 @@
(^|\.)sierrafriendsoftibet\.org$
(^|\.)sijihuisuo\.club$
(^|\.)sijihuisuo\.com$
(^|\.)sikaozhe1997\.github\.io$
(^|\.)silkbook\.com$
(^|\.)simbolostwitter\.com$
(^|\.)simplecd\.org$
......@@ -3742,11 +4018,13 @@
(^|\.)sis\.xxx$
(^|\.)sis001\.com$
(^|\.)sis001\.us$
(^|\.)site2unblock\.com$
(^|\.)site90\.net$
(^|\.)sitebro\.tw$
(^|\.)sitekreator\.com$
(^|\.)siteks\.uk\.to$
(^|\.)sitemaps\.org$
(^|\.)six-degrees\.io$
(^|\.)sixth\.biz$
(^|\.)sjrt\.org$
(^|\.)sjum\.cn$
......@@ -3783,6 +4061,7 @@
(^|\.)sobees\.com$
(^|\.)soc\.mil$
(^|\.)socialwhale\.com$
(^|\.)socks-proxy\.net$
(^|\.)sockscap64\.com$
(^|\.)sockslist\.net$
(^|\.)socrec\.org$
......@@ -3791,6 +4070,7 @@
(^|\.)softether-download\.com$
(^|\.)softether\.co\.jp$
(^|\.)softether\.org$
(^|\.)softsmirror\.cf$
(^|\.)softwarebychuck\.com$
(^|\.)softwaredownload\.gitbooks\.io$
(^|\.)sogclub\.com$
......@@ -3827,6 +4107,8 @@
(^|\.)sourcewadio\.com$
(^|\.)southnews\.com\.tw$
(^|\.)sowers\.org\.hk$
(^|\.)soylentnews\.org$
(^|\.)spaces\.hightail\.com$
(^|\.)spankbang\.com$
(^|\.)spankingtube\.com$
(^|\.)spankwire\.com$
......@@ -3836,6 +4118,7 @@
(^|\.)speedify\.com$
(^|\.)spem\.at$
(^|\.)spencertipping\.com$
(^|\.)spendee\.com$
(^|\.)spicevpn\.com$
(^|\.)spideroak\.com$
(^|\.)spike\.com$
......@@ -3854,12 +4137,17 @@
(^|\.)ss-link\.com$
(^|\.)ss\.carryzhou\.com$
(^|\.)ss\.levyhsu\.com$
(^|\.)ss\.pythonic\.life$
(^|\.)ss7\.vzw\.com$
(^|\.)ssglobal\.co$
(^|\.)ssglobal\.me$
(^|\.)ssh91\.com$
(^|\.)ssl\.webpack\.de$
(^|\.)ssl443\.org$
(^|\.)sspanel\.net$
(^|\.)sspro\.ml$
(^|\.)ssr\.tools$
(^|\.)ssrshare\.com$
(^|\.)sss\.camp$
(^|\.)sstmlt\.moe$
(^|\.)sstmlt\.net$
......@@ -3874,6 +4162,7 @@
(^|\.)stat\.gov\.tw$
(^|\.)static-economist\.com$
(^|\.)static\.comico\.tw$
(^|\.)static\.shemalez\.com$
(^|\.)static01\.nyt\.com$
(^|\.)staticflickr\.com$
(^|\.)statueofdemocracy\.org$
......@@ -3916,6 +4205,7 @@
(^|\.)sugumiru18\.com$
(^|\.)suissl\.com$
(^|\.)sujiatun\.wordpress\.com$
(^|\.)sukebei\.nyaa\.si$
(^|\.)sulian\.me$
(^|\.)summify\.com$
(^|\.)sumrando\.com$
......@@ -3927,19 +4217,23 @@
(^|\.)sunvpn\.net$
(^|\.)sunwinism\.joinbbs\.net$
(^|\.)suoluo\.org$
(^|\.)supchina\.com$
(^|\.)superfreevpn\.com$
(^|\.)superokayama\.com$
(^|\.)superpages\.com$
(^|\.)supervpn\.net$
(^|\.)superzooi\.com$
(^|\.)suppig\.net$
(^|\.)suprememastertv\.com$
(^|\.)surfeasy\.com$
(^|\.)surfeasy\.com\.au$
(^|\.)suroot\.com$
(^|\.)surrenderat20\.net$
(^|\.)sustainability\.google$
(^|\.)suyangg\.com$
(^|\.)svsfx\.com$
(^|\.)swagbucks\.com$
(^|\.)swissinfo\.ch$
(^|\.)swissvpn\.net$
(^|\.)switch1\.jp$
(^|\.)switchvpn\.net$
......@@ -3977,6 +4271,7 @@
(^|\.)taiwandc\.org$
(^|\.)taiwanjobs\.gov\.tw$
(^|\.)taiwanjustice\.com$
(^|\.)taiwanjustice\.net$
(^|\.)taiwankiss\.com$
(^|\.)taiwannation\.50webs\.com$
(^|\.)taiwannation\.com$
......@@ -3990,6 +4285,7 @@
(^|\.)taiwanyes\.ning\.com$
(^|\.)talk853\.com$
(^|\.)talkboxapp\.com$
(^|\.)talkcc\.com$
(^|\.)talkonly\.net$
(^|\.)tamiaode\.tk$
(^|\.)tanc\.org$
......@@ -4027,10 +4323,15 @@
(^|\.)tcpspeed\.com$
(^|\.)tcsofbc\.org$
(^|\.)tcsovi\.org$
(^|\.)tdm\.com\.mo$
(^|\.)teachparentstech\.org$
(^|\.)teamamericany\.com$
(^|\.)tech2\.in\.com$
(^|\.)techviz\.net$
(^|\.)teck\.in$
(^|\.)teco-hk\.org$
(^|\.)teco-mo\.org$
(^|\.)teddysun\.com$
(^|\.)teeniefuck\.net$
(^|\.)teensinasia\.com$
(^|\.)telecomspace\.com$
......@@ -4044,7 +4345,9 @@
(^|\.)tenacy\.com$
(^|\.)tensorflow\.org$
(^|\.)tenzinpalmo\.com$
(^|\.)terminus2049\.github\.io$
(^|\.)tew\.org$
(^|\.)textnow\.me$
(^|\.)th\.hao123\.com$
(^|\.)thaicn\.com$
(^|\.)thb\.gov\.tw$
......@@ -4067,6 +4370,7 @@
(^|\.)theinitium\.com$
(^|\.)thenewslens\.com$
(^|\.)thepiratebay\.org$
(^|\.)theporndude\.com$
(^|\.)theportalwiki\.com$
(^|\.)thereallove\.kr$
(^|\.)therock\.net\.nz$
......@@ -4080,6 +4384,8 @@
(^|\.)thetrotskymovie\.com$
(^|\.)thevivekspot\.com$
(^|\.)thewgo\.org$
(^|\.)theync\.com$
(^|\.)thinkgeek\.com$
(^|\.)thinkingtaiwan\.com$
(^|\.)thinkwithgoogle\.com$
(^|\.)thisav\.com$
......@@ -4190,6 +4496,8 @@
(^|\.)times\.hinet\.net$
(^|\.)timesofindia\.indiatimes\.com$
(^|\.)timsah\.com$
(^|\.)tinc-vpn\.org$
(^|\.)tineye\.com$
(^|\.)tintuc101\.com$
(^|\.)tiny\.cc$
(^|\.)tinychat\.com$
......@@ -4203,6 +4511,9 @@
(^|\.)tmdfish\.com$
(^|\.)tmi\.me$
(^|\.)tmpp\.org$
(^|\.)tn1\.shemalez\.com$
(^|\.)tn2\.shemalez\.com$
(^|\.)tn3\.shemalez\.com$
(^|\.)tnaflix\.com$
(^|\.)tngrnow\.com$
(^|\.)tngrnow\.net$
......@@ -4220,7 +4531,9 @@
(^|\.)toodoc\.com$
(^|\.)toonel\.net$
(^|\.)top\.tv$
(^|\.)top10vpn\.com$
(^|\.)top81\.ws$
(^|\.)topbtc\.com$
(^|\.)topic\.youthwant\.com\.tw$
(^|\.)topnews\.in$
(^|\.)toppornsites\.com$
......@@ -4242,6 +4555,7 @@
(^|\.)tosh\.comedycentral\.com$
(^|\.)totalvpn\.com$
(^|\.)toutiaoabc\.com$
(^|\.)toutyrater\.github\.io$
(^|\.)towngain\.com$
(^|\.)toypark\.in$
(^|\.)toythieves\.com$
......@@ -4276,6 +4590,7 @@
(^|\.)tt-rss\.org$
(^|\.)tt1069\.com$
(^|\.)tttan\.com$
(^|\.)ttvnw\.net$
(^|\.)tu8964\.com$
(^|\.)tubaholic\.com$
(^|\.)tube\.com$
......@@ -4284,6 +4599,7 @@
(^|\.)tubecup\.com$
(^|\.)tubegals\.com$
(^|\.)tubeislam\.com$
(^|\.)tubepornclassic\.com$
(^|\.)tubestack\.com$
(^|\.)tubewolf\.com$
(^|\.)tui\.orzdream\.com$
......@@ -4330,6 +4646,7 @@
(^|\.)tw\.bid\.yahoo\.com$
(^|\.)tw\.gigacircle\.com$
(^|\.)tw\.hao123\.com$
(^|\.)tw\.iqiyi\.com$
(^|\.)tw\.jiepang\.com$
(^|\.)tw\.knowledge\.yahoo\.com$
(^|\.)tw\.mall\.yahoo\.com$
......@@ -4377,6 +4694,7 @@
(^|\.)twibble\.de$
(^|\.)twibbon\.com$
(^|\.)twibs\.com$
(^|\.)twicountry\.org$
(^|\.)twicsy\.com$
(^|\.)twiends\.com$
(^|\.)twifan\.com$
......@@ -4398,6 +4716,8 @@
(^|\.)twit2d\.com$
(^|\.)twitbrowser\.net$
(^|\.)twitcause\.com$
(^|\.)twitch\.tv$
(^|\.)twitchcdn\.net$
(^|\.)twitgether\.com$
(^|\.)twitgoo\.com$
(^|\.)twitiq\.com$
......@@ -4449,6 +4769,7 @@
(^|\.)udnbkk\.com$
(^|\.)uforadio\.com\.tw$
(^|\.)ufreevpn\.com$
(^|\.)ug\.m\.wikipedia\.org$
(^|\.)ugo\.com$
(^|\.)uhdwallpapers\.org$
(^|\.)uhrp\.org$
......@@ -4457,6 +4778,7 @@
(^|\.)uighurbiz\.net$
(^|\.)ukcdp\.co\.uk$
(^|\.)ukliferadio\.co\.uk$
(^|\.)uku\.im$
(^|\.)ulike\.net$
(^|\.)ulop\.net$
(^|\.)ultrareach$
......@@ -4477,6 +4799,7 @@
(^|\.)uni\.cc$
(^|\.)unification\.net$
(^|\.)unification\.org\.tw$
(^|\.)unirule\.cloud$
(^|\.)unitedsocialpress\.com$
(^|\.)unix100\.com$
(^|\.)unknownspace\.org$
......@@ -4497,6 +4820,7 @@
(^|\.)uproxy\.org$
(^|\.)upwill\.org$
(^|\.)ur7s\.com$
(^|\.)uraban\.me$
(^|\.)urbansurvival\.com$
(^|\.)urchin\.com$
(^|\.)urlborg\.com$
......@@ -4519,6 +4843,7 @@
(^|\.)usus\.cc$
(^|\.)utopianpal\.com$
(^|\.)uu-gg\.com$
(^|\.)uukanshu\.com$
(^|\.)uvwxyz\.xyz$
(^|\.)uwants\.com$
(^|\.)uwants\.net$
......@@ -4542,6 +4867,7 @@
(^|\.)vanilla-jp\.com$
(^|\.)vanpeople\.com$
(^|\.)vansky\.com$
(^|\.)vaticannews\.va$
(^|\.)vatn\.org$
(^|\.)vcf-online\.org$
(^|\.)vcfbuilder\.org$
......@@ -4587,6 +4913,7 @@
(^|\.)vine\.co$
(^|\.)vinniev\.com$
(^|\.)vip-enterprise\.com$
(^|\.)virtualrealporn\.com$
(^|\.)visibletweets\.com$
(^|\.)vital247\.org$
(^|\.)viu\.com$
......@@ -4595,10 +4922,12 @@
(^|\.)vivatube\.com$
(^|\.)vivthomas\.com$
(^|\.)vizvaz\.com$
(^|\.)vjav\.com$
(^|\.)vjmedia\.com\.hk$
(^|\.)vllcs\.org$
(^|\.)vlog\.xuite\.net$
(^|\.)vmixcore\.com$
(^|\.)vmpsoft\.com$
(^|\.)vn\.hao123\.com$
(^|\.)vnet\.link$
(^|\.)voa-11\.akacast\.akamaistream\.net$
......@@ -4611,6 +4940,7 @@
(^|\.)voatibetanenglish\.com$
(^|\.)vocativ\.com$
(^|\.)vocn\.tv$
(^|\.)vod-abematv\.akamaized\.net$
(^|\.)vod\.wwe\.com$
(^|\.)vot\.org$
(^|\.)vovo2000\.com$
......@@ -4630,6 +4960,7 @@
(^|\.)vpndada\.com$
(^|\.)vpnfan\.com$
(^|\.)vpnfire\.com$
(^|\.)vpnfires\.biz$
(^|\.)vpnforgame\.net$
(^|\.)vpngate\.jp$
(^|\.)vpngate\.net$
......@@ -4660,8 +4991,10 @@
(^|\.)vpser\.net$
(^|\.)vraiesagesse\.net$
(^|\.)vrmtr\.com$
(^|\.)vrsmash\.com$
(^|\.)vtunnel\.com$
(^|\.)vuku\.cc$
(^|\.)vultryhw\.com$
(^|\.)w\.idaiwan\.com$
(^|\.)w3schools\.com$
(^|\.)waffle1999\.com$
......@@ -4699,6 +5032,7 @@
(^|\.)wdf5\.com$
(^|\.)wearehairy\.com$
(^|\.)wearn\.com$
(^|\.)web\.dev$
(^|\.)web2project\.net$
(^|\.)webbang\.net$
(^|\.)webevader\.org$
......@@ -4716,6 +5050,7 @@
(^|\.)weekmag\.info$
(^|\.)wefightcensorship\.org$
(^|\.)wefong\.com$
(^|\.)wego\.here\.com$
(^|\.)weiboleak\.com$
(^|\.)weiboscope\.jmsc\.hku\.hk$
(^|\.)weihuo\.org$
......@@ -4730,6 +5065,7 @@
(^|\.)wenhui\.ch$
(^|\.)wenxuecity\.com$
(^|\.)wenyunchao\.com$
(^|\.)wenzhao\.ca$
(^|\.)westca\.com$
(^|\.)westernshugdensociety\.org$
(^|\.)westernwolves\.com$
......@@ -4744,7 +5080,9 @@
(^|\.)wha\.la$
(^|\.)whatblocked\.com$
(^|\.)whatbrowser\.org$
(^|\.)whatsapp\.com$
(^|\.)whatsapp\.net$
(^|\.)whatsonweibo\.com$
(^|\.)wheatseeds\.org$
(^|\.)wheelockslatin\.com$
(^|\.)whereiswerner\.com$
......@@ -4778,6 +5116,7 @@
(^|\.)wikimapia\.org$
(^|\.)wikiwiki\.jp$
(^|\.)wildammo\.com$
(^|\.)williamhill\.com$
(^|\.)willw\.net$
(^|\.)windowsphoneme\.com$
(^|\.)windscribe\.com$
......@@ -4813,6 +5152,7 @@
(^|\.)wordpress\.com$
(^|\.)workatruna\.com$
(^|\.)workerdemo\.org\.hk$
(^|\.)workerempowerment\.org$
(^|\.)workersthebig\.net$
(^|\.)worldcat\.org$
(^|\.)worldjournal\.com$
......@@ -4910,6 +5250,7 @@
(^|\.)xanga\.com$
(^|\.)xbabe\.com$
(^|\.)xbookcn\.com$
(^|\.)xbtce\.com$
(^|\.)xcafe\.in$
(^|\.)xcity\.jp$
(^|\.)xcritic\.com$
......@@ -4918,6 +5259,7 @@
(^|\.)xfm\.pp\.ru$
(^|\.)xgmyd\.com$
(^|\.)xhamster\.com$
(^|\.)xianba\.net$
(^|\.)xianchawang\.net$
(^|\.)xianjian\.tw$
(^|\.)xianqiao\.net$
......@@ -4929,6 +5271,7 @@
(^|\.)xiaoma\.org$
(^|\.)xiezhua\.com$
(^|\.)xihua\.es$
(^|\.)xijie\.wordpress\.com$
(^|\.)xing\.com$
(^|\.)xinhuanet\.org$
(^|\.)xinmiao\.com\.hk$
......@@ -4951,6 +5294,7 @@
(^|\.)xn--czq75pvv1aj5c\.org$
(^|\.)xn--i2ru8q2qg\.com$
(^|\.)xn--ngstr-lra8j\.com$
(^|\.)xn--oiq\.cc$
(^|\.)xn--p8j9a0d9c9a\.xn--q9jyb4c$
(^|\.)xnxx\.com$
(^|\.)xpdo\.net$
......@@ -5012,11 +5356,15 @@
(^|\.)yinlei\.org$
(^|\.)yipub\.com$
(^|\.)yizhihongxing\.com$
(^|\.)yobit\.net$
(^|\.)yobt\.com$
(^|\.)yobt\.tv$
(^|\.)yogichen\.org$
(^|\.)yolasite\.com$
(^|\.)yomiuri\.co\.jp$
(^|\.)yong\.hu$
(^|\.)yorkbbs\.ca$
(^|\.)you-get\.org$
(^|\.)youdontcare\.com$
(^|\.)youjizz\.com$
(^|\.)youmaker\.com$
......@@ -5041,6 +5389,7 @@
(^|\.)youtubeeducation\.com$
(^|\.)youtubegaming\.com$
(^|\.)youversion\.com$
(^|\.)youwin\.com$
(^|\.)youxu\.info$
(^|\.)yt\.be$
(^|\.)ytht\.net$
......@@ -5060,10 +5409,12 @@
(^|\.)zacebook\.com$
(^|\.)zalmos\.com$
(^|\.)zannel\.com$
(^|\.)zaobao\.com$
(^|\.)zaobao\.com\.sg$
(^|\.)zaozon\.com$
(^|\.)zapto\.org$
(^|\.)zattoo\.com$
(^|\.)zb\.com$
(^|\.)zdnet\.com\.tw$
(^|\.)zello\.com$
(^|\.)zengjinyan\.org$
......@@ -5075,9 +5426,11 @@
(^|\.)zgsddh\.com$
(^|\.)zgzcjj\.net$
(^|\.)zh-yue\.wikipedia\.org$
(^|\.)zh\.bitterwinter\.org$
(^|\.)zh\.ecdm\.wikia\.com$
(^|\.)zh\.m\.wikipedia\.org$
(^|\.)zh\.pokerstrategy\.com$
(^|\.)zh\.pttpedia\.wikia\.com$
(^|\.)zh\.uncyclopedia\.wikia\.com$
(^|\.)zh\.wikinews\.org$
(^|\.)zh\.wikipedia\.org$
......@@ -5135,4 +5488,5 @@
(^|\.)zyns\.com$
(^|\.)zyzc9\.com$
(^|\.)zzcartoon\.com$
(^|\.)zzcloud\.me$
(^|\.)zzux\.com$
......@@ -32,6 +32,7 @@ import java.io.File
import java.io.IOException
import java.io.Reader
import java.net.URL
import java.net.URLConnection
class Acl {
companion object {
......@@ -151,11 +152,11 @@ class Acl {
fromReader(getFile(id).bufferedReader())
} catch (_: IOException) { this }
fun flatten(depth: Int): Acl {
suspend fun flatten(depth: Int, connect: suspend (URL) -> URLConnection): Acl {
if (depth > 0) for (url in urls.asIterable()) {
val child = Acl()
try {
child.fromReader(url.openStream().bufferedReader(), bypass).flatten(depth - 1)
child.fromReader(connect(url).getInputStream().bufferedReader(), bypass).flatten(depth - 1, connect)
} catch (e: IOException) {
e.printStackTrace()
continue
......
......@@ -28,6 +28,7 @@ import android.os.DeadObjectException
import android.os.Handler
import android.os.IBinder
import android.os.RemoteException
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.bg.ProxyService
import com.github.shadowsocks.bg.TransproxyService
import com.github.shadowsocks.bg.VpnService
......@@ -51,7 +52,7 @@ class ShadowsocksConnection(private val handler: Handler = Handler(),
}
interface Callback {
fun stateChanged(state: Int, profileName: String?, msg: String?)
fun stateChanged(state: BaseService.State, profileName: String?, msg: String?)
fun trafficUpdated(profileId: Long, stats: TrafficStats) { }
fun trafficPersisted(profileId: Long) { }
......@@ -68,13 +69,16 @@ class ShadowsocksConnection(private val handler: Handler = Handler(),
private var callback: Callback? = null
private val serviceCallback = object : IShadowsocksServiceCallback.Stub() {
override fun stateChanged(state: Int, profileName: String?, msg: String?) {
handler.post { callback!!.stateChanged(state, profileName, msg) }
val callback = callback ?: return
handler.post { callback.stateChanged(BaseService.State.values()[state], profileName, msg) }
}
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
handler.post { callback!!.trafficUpdated(profileId, stats) }
val callback = callback ?: return
handler.post { callback.trafficUpdated(profileId, stats) }
}
override fun trafficPersisted(profileId: Long) {
handler.post { callback!!.trafficPersisted(profileId) }
val callback = callback ?: return
handler.post { callback.trafficPersisted(profileId) }
}
}
private var binder: IBinder? = null
......@@ -105,14 +109,14 @@ class ShadowsocksConnection(private val handler: Handler = Handler(),
override fun onServiceDisconnected(name: ComponentName?) {
unregisterCallback()
callback!!.onServiceDisconnected()
callback?.onServiceDisconnected()
service = null
binder = null
}
override fun binderDied() {
service = null
handler.post(callback!!::onBinderDied)
callback?.also { handler.post(it::onBinderDied) }
}
private fun unregisterCallback() {
......
......@@ -39,9 +39,11 @@ import com.github.shadowsocks.plugin.PluginManager
import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.broadcastReceiver
import com.github.shadowsocks.utils.printLog
import com.github.shadowsocks.utils.readableMessage
import com.google.firebase.analytics.FirebaseAnalytics
import kotlinx.coroutines.*
import java.io.File
import java.net.BindException
import java.net.InetAddress
import java.net.URL
import java.net.UnknownHostException
......@@ -51,20 +53,22 @@ import java.util.*
* This object uses WeakMap to simulate the effects of multi-inheritance.
*/
object BaseService {
enum class State(val canStop: Boolean = false) {
/**
* IDLE state is only used by UI and will never be returned by BaseService.
* Idle state is only used by UI and will never be returned by BaseService.
*/
const val IDLE = 0
const val CONNECTING = 1
const val CONNECTED = 2
const val STOPPING = 3
const val STOPPED = 4
Idle,
Connecting(true),
Connected(true),
Stopping,
Stopped,
}
const val CONFIG_FILE = "shadowsocks.conf"
const val CONFIG_FILE_UDP = "shadowsocks-udp.conf"
class Data internal constructor(private val service: Interface) {
var state = STOPPED
var state = State.Stopped
var processes: GuardedProcessPool? = null
var proxy: ProxyInstance? = null
var udpFallback: ProxyInstance? = null
......@@ -81,7 +85,7 @@ object BaseService {
val binder = Binder(this)
var connectingJob: Job? = null
fun changeState(s: Int, msg: String? = null) {
fun changeState(s: State, msg: String? = null) {
if (state == s && msg == null) return
binder.stateChanged(s, msg)
state = s
......@@ -102,7 +106,7 @@ object BaseService {
private val bandwidthListeners = mutableMapOf<IBinder, Long>() // the binder is the real identifier
private val handler = Handler()
override fun getState(): Int = data!!.state
override fun getState(): Int = data!!.state.ordinal
override fun getProfileName(): String = data!!.proxy?.profile?.name ?: "Idle"
override fun registerCallback(cb: IShadowsocksServiceCallback) {
......@@ -124,12 +128,12 @@ object BaseService {
handler.postDelayed(this::onTimeout, bandwidthListeners.values.min() ?: return)
}
private fun onTimeout() {
val proxies = listOfNotNull(data!!.proxy, data!!.udpFallback)
val proxies = listOfNotNull(data?.proxy, data?.udpFallback)
val stats = proxies
.map { Pair(it.profile.id, it.trafficMonitor?.requestUpdate()) }
.filter { it.second != null }
.map { Triple(it.first, it.second!!.first, it.second!!.second) }
if (stats.any { it.third } && state == CONNECTED && bandwidthListeners.isNotEmpty()) {
if (stats.any { it.third } && data!!.state == State.Connected && bandwidthListeners.isNotEmpty()) {
val sum = stats.fold(TrafficStats()) { a, b -> a + b.second }
broadcast { item ->
if (bandwidthListeners.contains(item.asBinder())) {
......@@ -145,7 +149,7 @@ object BaseService {
val wasEmpty = bandwidthListeners.isEmpty()
if (bandwidthListeners.put(cb.asBinder(), timeout) == null) {
if (wasEmpty) registerTimeout()
if (state != CONNECTED) return
if (data!!.state != State.Connected) return
var sum = TrafficStats()
val proxy = data!!.proxy ?: return
proxy.trafficMonitor?.out.also { stats ->
......@@ -177,9 +181,9 @@ object BaseService {
callbacks.unregister(cb)
}
fun stateChanged(s: Int, msg: String?) {
fun stateChanged(s: State, msg: String?) {
val profileName = profileName
broadcast { it.stateChanged(s, profileName, msg) }
broadcast { it.stateChanged(s.ordinal, profileName, msg) }
}
fun trafficPersisted(ids: List<Long>) {
......@@ -211,9 +215,9 @@ object BaseService {
return
}
val s = data.state
when (s) {
STOPPED -> startRunner()
CONNECTED -> stopRunner(true)
when {
s == State.Stopped -> startRunner()
s.canStop -> stopRunner(true)
else -> Crashlytics.log(Log.WARN, tag, "Illegal state when invoking use: $s")
}
}
......@@ -249,11 +253,12 @@ object BaseService {
}
fun stopRunner(restart: Boolean = false, msg: String? = null) {
if (data.state == STOPPING) return
if (data.state == State.Stopping) return
// channge the state
data.changeState(STOPPING)
data.changeState(State.Stopping)
GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) {
Core.analytics.logEvent("stop", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
data.connectingJob?.cancelAndJoin() // ensure stop connecting first
this@Interface as Service
// we use a coroutineScope here to allow clean-up in parallel
coroutineScope {
......@@ -278,7 +283,7 @@ object BaseService {
}
// change the state
data.changeState(STOPPED, msg)
data.changeState(State.Stopped, msg)
// stop the service if nothing has bound to it
if (restart) startRunner() else stopSelf()
......@@ -291,7 +296,7 @@ object BaseService {
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val data = data
if (data.state != STOPPED) return Service.START_NOT_STICKY
if (data.state != State.Stopped) return Service.START_NOT_STICKY
val profilePair = Core.currentProfile
this as Context
if (profilePair == null) {
......@@ -318,7 +323,7 @@ object BaseService {
data.notification = createNotification(profile.formattedName)
Core.analytics.logEvent("start", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
data.changeState(CONNECTING)
data.changeState(State.Connecting)
data.connectingJob = GlobalScope.launch(Dispatchers.Main) {
try {
Executable.killAll() // clean up old processes
......@@ -328,8 +333,7 @@ object BaseService {
data.processes = GuardedProcessPool {
printLog(it)
data.connectingJob?.cancelAndJoin()
stopRunner(false, it.localizedMessage)
stopRunner(false, it.readableMessage)
}
startProcesses()
......@@ -337,16 +341,18 @@ object BaseService {
data.udpFallback?.scheduleUpdate()
RemoteConfig.fetch()
data.changeState(CONNECTED)
data.changeState(State.Connected)
} catch (_: CancellationException) {
// if the job was cancelled, it is canceller's responsibility to call stopRunner
} catch (_: UnknownHostException) {
stopRunner(false, getString(R.string.invalid_server))
} catch (exc: Throwable) {
if (exc !is PluginManager.PluginNotFoundException && exc !is VpnService.NullConnectionException) {
if (exc !is PluginManager.PluginNotFoundException &&
exc !is BindException &&
exc !is VpnService.NullConnectionException) {
printLog(exc)
}
stopRunner(false, "${getString(R.string.service_failed)}: ${exc.localizedMessage}")
stopRunner(false, "${getString(R.string.service_failed)}: ${exc.readableMessage}")
} finally {
data.connectingJob = null
}
......
......@@ -26,7 +26,6 @@ import android.system.OsConstants
import android.text.TextUtils
import android.util.Log
import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.Core.app
import java.io.File
import java.io.FileNotFoundException
......@@ -40,11 +39,11 @@ object Executable {
fun killAll() {
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }) {
val exe = File(try {
File(process, "cmdline").readText()
} catch (ignore: FileNotFoundException) {
File(process, "cmdline").inputStream().bufferedReader().readText()
} catch (_: FileNotFoundException) {
continue
}.split(Character.MIN_VALUE, limit = 2).first())
if (exe.parent == app.applicationInfo.nativeLibraryDir && EXECUTABLES.contains(exe.name)) try {
if (EXECUTABLES.contains(exe.name)) try {
Os.kill(process.name.toInt(), OsConstants.SIGKILL)
} catch (e: ErrnoException) {
if (e.errno != OsConstants.ESRCH) {
......
......@@ -61,7 +61,7 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
conn.doOutput = true
val proxies = try {
withTimeoutOrNull(30_000) {
withTimeoutOrNull(10_000) {
withContext(Dispatchers.IO) {
conn.outputStream.bufferedWriter().use {
it.write("sig=" + Base64.encodeToString(mdg.digest(), Base64.DEFAULT))
......@@ -80,7 +80,9 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
profile.method = proxy[3].trim()
}
if (route == Acl.CUSTOM_RULES) Acl.save(Acl.CUSTOM_RULES, Acl.customRules.flatten(10))
if (route == Acl.CUSTOM_RULES) withContext(Dispatchers.IO) {
Acl.save(Acl.CUSTOM_RULES, Acl.customRules.flatten(10, service::openConnection))
}
// it's hard to resolve DNS on a specific interface so we'll do it here
if (profile.host.parseNumericAddress() == null) profile.host = withTimeoutOrNull(10_000) {
......
......@@ -94,7 +94,7 @@ class ServiceNotification(private val service: BaseService.Interface, profileNam
}
private fun update(action: String?, forceShow: Boolean = false) {
if (forceShow || service.data.state == BaseService.CONNECTED) when (action) {
if (forceShow || service.data.state == BaseService.State.Connected) when (action) {
Intent.ACTION_SCREEN_OFF -> {
setVisible(false, forceShow)
unregisterCallback() // unregister callback to save battery
......
......@@ -21,6 +21,7 @@
package com.github.shadowsocks.database
import android.net.Uri
import android.os.Parcelable
import android.util.Base64
import android.util.Log
import android.util.LongSparseArray
......@@ -32,6 +33,7 @@ import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.asIterable
import com.github.shadowsocks.utils.parsePort
import kotlinx.android.parcel.Parcelize
import org.json.JSONArray
import org.json.JSONObject
import org.json.JSONTokener
......@@ -41,7 +43,31 @@ import java.net.URISyntaxException
import java.util.*
@Entity
class Profile : Serializable {
@Parcelize
data class Profile(
@PrimaryKey(autoGenerate = true)
var id: Long = 0,
var name: String? = "",
var host: String = "198.199.101.152",
var remotePort: Int = 8388,
var password: String = "u1rRWTssNv0p",
var method: String = "aes-256-cfb",
var route: String = "all",
var remoteDns: String = "8.8.8.8",
var proxyApps: Boolean = false,
var bypass: Boolean = false,
var udpdns: Boolean = false,
var ipv6: Boolean = true,
var individual: String = "",
var tx: Long = 0,
var rx: Long = 0,
var userOrder: Long = 0,
var plugin: String? = null,
var udpFallback: Long? = null,
@Ignore // not persisted in db, only used by direct boot
var dirty: Boolean = false
) : Parcelable, Serializable {
companion object {
private const val TAG = "ShadowParser"
private const val serialVersionUID = 0L
......@@ -200,29 +226,6 @@ class Profile : Serializable {
fun deleteAll(): Int
}
@PrimaryKey(autoGenerate = true)
var id: Long = 0
var name: String? = ""
var host: String = "198.199.101.152"
var remotePort: Int = 8388
var password: String = "u1rRWTssNv0p"
var method: String = "aes-256-cfb"
var route: String = "all"
var remoteDns: String = "8.8.8.8"
var proxyApps: Boolean = false
var bypass: Boolean = false
var udpdns: Boolean = false
var ipv6: Boolean = true
var individual: String = ""
var tx: Long = 0
var rx: Long = 0
var userOrder: Long = 0
var plugin: String? = null
var udpFallback: Long? = null
@Ignore // not persisted in db, only used by direct boot
var dirty: Boolean = false
val formattedAddress get() = (if (host.contains(":")) "[%s]:%d" else "%s:%d").format(host, remotePort)
val formattedName get() = if (name.isNullOrEmpty()) formattedAddress else name!!
......
......@@ -21,11 +21,14 @@
package com.github.shadowsocks.database
import android.database.sqlite.SQLiteCantOpenDatabaseException
import android.util.LongSparseArray
import com.github.shadowsocks.Core
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.printLog
import org.json.JSONArray
import java.io.IOException
import java.io.InputStream
import java.sql.SQLException
/**
......@@ -36,6 +39,7 @@ object ProfileManager {
interface Listener {
fun onAdd(profile: Profile)
fun onRemove(profileId: Long)
fun onCleared()
}
var listener: Listener? = null
......@@ -48,6 +52,36 @@ object ProfileManager {
return profile
}
fun createProfilesFromJson(jsons: Sequence<InputStream>, replace: Boolean = false) {
val profiles = if (replace) getAllProfiles()?.associateBy { it.formattedAddress } else null
val feature = if (replace) {
profiles?.values?.singleOrNull { it.id == DataStore.profileId }
} else Core.currentProfile?.first
val lazyClear = lazy { clear() }
var result: Exception? = null
for (json in jsons) try {
Profile.parseJson(json.bufferedReader().readText(), feature) {
if (replace) {
lazyClear.value
// if two profiles has the same address, treat them as the same profile and copy stats over
profiles?.get(it.formattedAddress)?.apply {
it.tx = tx
it.rx = rx
}
}
createProfile(it)
}
} catch (e: Exception) {
if (result == null) result = e else result.addSuppressed(e)
}
if (result != null) throw result
}
fun serializeToJson(profiles: List<Profile>? = getAllProfiles()): JSONArray? {
if (profiles == null) return null
val lookup = LongSparseArray<Profile>(profiles.size).apply { profiles.forEach { put(it.id, it) } }
return JSONArray(profiles.map { it.toJson(lookup) }.toTypedArray())
}
/**
* Note: It's caller's responsibility to update DirectBoot profile if necessary.
*/
......@@ -78,6 +112,7 @@ object ProfileManager {
fun clear() = PrivateDatabase.profileDao.deleteAll().also {
// listener is not called since this won't be used in mobile submodule
DirectBoot.clean()
listener?.onCleared()
}
@Throws(IOException::class)
......
......@@ -20,6 +20,7 @@
package com.github.shadowsocks.net
import android.os.Build
import com.github.shadowsocks.utils.printLog
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
......@@ -65,23 +66,31 @@ class ChannelMonitor : Thread("ChannelMonitor") {
start()
}
/**
* Prevent NetworkOnMainThreadException because people enable strict mode for no reasons.
*/
private suspend fun WritableByteChannel.writeCompat(src: ByteBuffer) =
if (Build.VERSION.SDK_INT <= 23) withContext(Dispatchers.Default) { write(src) } else write(src)
suspend fun register(channel: SelectableChannel, ops: Int, block: (SelectionKey) -> Unit): SelectionKey {
val registration = Registration(channel, ops, block)
pendingRegistrations.send(registration)
ByteBuffer.allocateDirect(1).also { junk ->
loop@ while (running) when (registrationPipe.sink().write(junk)) {
loop@ while (running) when (registrationPipe.sink().writeCompat(junk)) {
0 -> kotlinx.coroutines.yield()
1 -> break@loop
else -> throw IOException("Failed to register in the channel")
}
}
if (!running) throw ClosedChannelException()
if (!running) throw CancellationException()
return registration.result.await()
}
suspend fun wait(channel: SelectableChannel, ops: Int) = CompletableDeferred<SelectionKey>().run {
register(channel, ops) {
if (it.isValid) it.interestOps(0) // stop listening
if (it.isValid) try {
it.interestOps(0) // stop listening
} catch (_: CancelledKeyException) { }
complete(it)
}
await()
......
......@@ -68,7 +68,7 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
private fun prepareDnsResponse(request: Message) = Message(request.header.id).apply {
header.setFlag(Flags.QR.toInt()) // this is a response
if (request.header.getFlag(Flags.RD.toInt())) header.setFlag(Flags.RD.toInt())
addRecord(request.question, Section.QUESTION)
request.question?.also { addRecord(it, Section.QUESTION) }
}
}
private val monitor = ChannelMonitor()
......@@ -99,30 +99,30 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
printLog(e)
return forward(packet)
}
return coroutineScope {
return supervisorScope {
val remote = async { withTimeout(TIMEOUT) { forward(packet) } }
try {
if (forwardOnly || request.header.opcode != Opcode.QUERY) return@coroutineScope remote.await()
if (forwardOnly || request.header.opcode != Opcode.QUERY) return@supervisorScope remote.await()
val question = request.question
if (question?.type != Type.A) return@coroutineScope remote.await()
if (question?.type != Type.A) return@supervisorScope remote.await()
val host = question.name.toString(true)
if (remoteDomainMatcher?.containsMatchIn(host) == true) return@coroutineScope remote.await()
if (remoteDomainMatcher?.containsMatchIn(host) == true) return@supervisorScope remote.await()
val localResults = try {
withTimeout(TIMEOUT) { GlobalScope.async(Dispatchers.IO) { localResolver(host) }.await() }
} catch (_: TimeoutCancellationException) {
Crashlytics.log(Log.WARN, TAG, "Local resolving timed out, falling back to remote resolving")
return@coroutineScope remote.await()
return@supervisorScope remote.await()
} catch (_: UnknownHostException) {
return@coroutineScope remote.await()
return@supervisorScope remote.await()
}
if (localResults.isEmpty()) return@coroutineScope remote.await()
if (localResults.isEmpty()) return@supervisorScope remote.await()
if (localIpMatcher.isEmpty() || localIpMatcher.any { subnet -> localResults.any(subnet::matches) }) {
remote.cancel()
ByteBuffer.wrap(prepareDnsResponse(request).apply {
header.setFlag(Flags.RA.toInt()) // recursion available
for (address in localResults) addRecord(when (address) {
is Inet4Address -> ARecord(request.question.name, DClass.IN, TTL, address)
is Inet6Address -> AAAARecord(request.question.name, DClass.IN, TTL, address)
is Inet4Address -> ARecord(question.name, DClass.IN, TTL, address)
is Inet6Address -> AAAARecord(question.name, DClass.IN, TTL, address)
else -> throw IllegalStateException("Unsupported address $address")
}, Section.ANSWER)
}.toWire())
......@@ -159,7 +159,7 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
check(channel.send(remoteDns.udpWrap(packet), proxy) > 0)
monitor.wait(channel, SelectionKey.OP_READ)
val result = remoteDns.udpReceiveBuffer(UDP_PACKET_SIZE)
check(channel.receive(result) == proxy)
while (channel.receive(result) != proxy) result.clear()
result.flip()
remoteDns.udpUnwrap(result)
result
......
......@@ -65,7 +65,7 @@ abstract class LocalSocketListener(name: String, socketFile: File) : Thread(name
open fun shutdown(scope: CoroutineScope) {
running = false
localSocket.fileDescriptor.apply {
localSocket.fileDescriptor?.apply {
// see also: https://issuetracker.google.com/issues/36945762#comment15
if (valid()) try {
Os.shutdown(this, OsConstants.SHUT_RDWR)
......
......@@ -20,6 +20,7 @@
package com.github.shadowsocks.net
import com.github.shadowsocks.utils.readableMessage
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
import java.io.File
......@@ -57,7 +58,7 @@ object TcpFastOpen {
ProcessBuilder("su", "-c", "echo 3 > $PATH").redirectErrorStream(true).start()
.inputStream.bufferedReader().readText()
} catch (e: IOException) {
e.localizedMessage
e.readableMessage
}
}
fun enableTimeout() = runBlocking { withTimeoutOrNull(1000) { enable() } }
......
......@@ -36,5 +36,5 @@ class DeviceStorageApp(context: Context) : Application() {
* Thou shalt not get the REAL underlying application context which would no longer be operating under device
* protected storage.
*/
override fun getApplicationContext(): Context = this
override fun getApplicationContext() = this
}
......@@ -42,6 +42,8 @@ import kotlinx.coroutines.launch
import java.net.HttpURLConnection
import java.net.InetAddress
val Throwable.readableMessage get() = localizedMessage ?: javaClass.name
private val parseNumericAddress by lazy {
InetAddress::class.java.getDeclaredMethod("parseNumericAddress", String::class.java).apply {
isAccessible = true
......
Subproject commit 7ad2df961505ecddb44984ce34ea86874cd2ecc4
Subproject commit b430124ed82973d4edf345dfadbe556f93377484
......@@ -97,6 +97,7 @@
<string name="action_export">Export to Clipboard</string>
<string name="action_import">Import from Clipboard</string>
<string name="action_import_file">Import from file…</string>
<string name="action_replace_file">Replace from file…</string>
<string name="action_export_msg">Successfully export!</string>
<string name="action_export_err">Failed to export.</string>
<string name="action_import_msg">Successfully import!</string>
......
......@@ -5,6 +5,7 @@ import java.util.regex.Pattern
apply plugin: 'com.android.application'
apply plugin: 'io.fabric'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
def getCurrentFlavor() {
String task = getGradle().getStartParameter().getTaskRequests().toString()
......@@ -49,6 +50,10 @@ android {
new File(project(':core').buildDir, "intermediates/bundles/${getCurrentFlavor()}/jni")
}
androidExtensions {
experimental = true
}
dependencies {
implementation project(':core')
implementation 'androidx.browser:browser:1.0.0'
......
......@@ -88,8 +88,8 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
portTransproxy.isEnabled = enabledTransproxy
true
}
val listener: (Int) -> Unit = {
if (it == BaseService.STOPPED) {
val listener: (BaseService.State) -> Unit = {
if (it == BaseService.State.Stopped) {
tfo.isEnabled = true
serviceMode.isEnabled = true
portProxy.isEnabled = true
......
......@@ -23,6 +23,7 @@ package com.github.shadowsocks
import android.app.Activity
import android.app.backup.BackupManager
import android.content.ActivityNotFoundException
import android.content.DialogInterface
import android.content.Intent
import android.net.VpnService
import android.nfc.NdefMessage
......@@ -30,6 +31,7 @@ import android.nfc.NfcAdapter
import android.os.Bundle
import android.os.DeadObjectException
import android.os.Handler
import android.os.Parcelable
import android.util.Log
import android.view.KeyCharacterMap
import android.view.KeyEvent
......@@ -51,6 +53,8 @@ import com.github.shadowsocks.aidl.TrafficStats
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.AlertDialogFragment
import com.github.shadowsocks.plugin.Empty
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.utils.Key
......@@ -58,6 +62,7 @@ import com.github.shadowsocks.widget.ServiceButton
import com.github.shadowsocks.widget.StatsBar
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.parcel.Parcelize
class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPreferenceDataStoreChangeListener,
NavigationView.OnNavigationItemSelectedListener {
......@@ -65,7 +70,18 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
private const val TAG = "ShadowsocksMainActivity"
private const val REQUEST_CONNECT = 1
var stateListener: ((Int) -> Unit)? = null
var stateListener: ((BaseService.State) -> Unit)? = null
}
@Parcelize
data class ProfilesArg(val profiles: List<Profile>) : Parcelable
class ImportProfilesDialogFragment : AlertDialogFragment<ProfilesArg, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setTitle(R.string.add_profile_dialog)
setPositiveButton(R.string.yes) { _, _ -> arg.profiles.forEach { ProfileManager.createProfile(it) } }
setNegativeButton(R.string.no, null)
setMessage(arg.profiles.joinToString("\n"))
}
}
// UI
......@@ -91,12 +107,12 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
}
// service
var state = BaseService.IDLE
override fun stateChanged(state: Int, profileName: String?, msg: String?) = changeState(state, msg, true)
var state = BaseService.State.Idle
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) = changeState(state, msg, true)
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
if (profileId == 0L) this@MainActivity.stats.updateTraffic(
stats.txRate, stats.rxRate, stats.txTotal, stats.rxTotal)
if (state != BaseService.STOPPING) {
if (state != BaseService.State.Stopping) {
(supportFragmentManager.findFragmentById(R.id.fragment_holder) as? ToolbarFragment)
?.onTrafficUpdated(profileId, stats)
}
......@@ -105,7 +121,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
ProfilesFragment.instance?.onTrafficPersisted(profileId)
}
private fun changeState(state: Int, msg: String? = null, animate: Boolean = false) {
private fun changeState(state: BaseService.State, msg: String? = null, animate: Boolean = false) {
fab.changeState(state, animate)
stats.changeState(state)
if (msg != null) snackbar(getString(R.string.vpn_error, msg)).show()
......@@ -115,7 +131,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
}
private fun toggle() = when {
state == BaseService.CONNECTED -> Core.stopService()
state.canStop -> Core.stopService()
DataStore.serviceMode == Key.modeVpn -> {
val intent = VpnService.prepare(this)
if (intent != null) startActivityForResult(intent, REQUEST_CONNECT)
......@@ -127,11 +143,11 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
private val handler = Handler()
private val connection = ShadowsocksConnection(handler, true)
override fun onServiceConnected(service: IShadowsocksService) = changeState(try {
service.state
BaseService.State.values()[service.state]
} catch (_: DeadObjectException) {
BaseService.IDLE
BaseService.State.Idle
})
override fun onServiceDisconnected() = changeState(BaseService.IDLE)
override fun onServiceDisconnected() = changeState(BaseService.State.Idle)
override fun onBinderDied() {
connection.disconnect(this)
connection.connect(this, this)
......@@ -152,7 +168,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_main)
stats = findViewById(R.id.stats)
stats.setOnClickListener { if (state == BaseService.CONNECTED) stats.testConnection() }
stats.setOnClickListener { if (state == BaseService.State.Connected) stats.testConnection() }
drawer = findViewById(R.id.drawer)
navigation = findViewById(R.id.navigation)
navigation.setNavigationItemSelectedListener(this)
......@@ -164,7 +180,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
fab = findViewById(R.id.fab)
fab.setOnClickListener { toggle() }
changeState(BaseService.IDLE) // reset everything to init state
changeState(BaseService.State.Idle) // reset everything to init state
connection.connect(this, this)
DataStore.publicStore.registerChangeListener(this)
......@@ -188,17 +204,8 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
}
if (sharedStr.isNullOrEmpty()) return
val profiles = Profile.findAllUrls(sharedStr, Core.currentProfile?.first).toList()
if (profiles.isEmpty()) {
snackbar().setText(R.string.profile_invalid_input).show()
return
}
AlertDialog.Builder(this)
.setTitle(R.string.add_profile_dialog)
.setPositiveButton(R.string.yes) { _, _ -> profiles.forEach { ProfileManager.createProfile(it) } }
.setNegativeButton(R.string.no, null)
.setMessage(profiles.joinToString("\n"))
.create()
.show()
if (profiles.isEmpty()) snackbar().setText(R.string.profile_invalid_input).show()
else ImportProfilesDialogFragment().withArg(ProfilesArg(profiles)).show(supportFragmentManager, null)
}
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) {
......
......@@ -21,12 +21,15 @@
package com.github.shadowsocks
import android.app.Activity
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.github.shadowsocks.plugin.AlertDialogFragment
import com.github.shadowsocks.plugin.Empty
import com.github.shadowsocks.plugin.PluginContract
import com.github.shadowsocks.preference.DataStore
......@@ -35,6 +38,15 @@ class ProfileConfigActivity : AppCompatActivity() {
const val REQUEST_CODE_PLUGIN_HELP = 1
}
class UnsavedChangesDialogFragment : AlertDialogFragment<Empty, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setTitle(R.string.unsaved_changes_prompt)
setPositiveButton(R.string.yes, listener)
setNegativeButton(R.string.no, listener)
setNeutralButton(android.R.string.cancel, null)
}
}
private val child by lazy { supportFragmentManager.findFragmentById(R.id.content) as ProfileConfigFragment }
override fun onCreate(savedInstanceState: Bundle?) {
......@@ -59,13 +71,8 @@ class ProfileConfigActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem) = child.onOptionsItemSelected(item)
override fun onBackPressed() {
if (DataStore.dirty) AlertDialog.Builder(this)
.setTitle(R.string.unsaved_changes_prompt)
.setPositiveButton(R.string.yes) { _, _ -> child.saveAndExit() }
.setNegativeButton(R.string.no) { _, _ -> finish() }
.setNeutralButton(android.R.string.cancel, null)
.create()
.show() else super.onBackPressed()
if (DataStore.dirty) UnsavedChangesDialogFragment().show(child, ProfileConfigFragment.REQUEST_UNSAVED_CHANGES)
else super.onBackPressed()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
......
......@@ -22,23 +22,24 @@ package com.github.shadowsocks
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.preference.*
import com.github.shadowsocks.Core.app
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.plugin.PluginContract
import com.github.shadowsocks.plugin.PluginManager
import com.github.shadowsocks.plugin.PluginOptions
import com.github.shadowsocks.plugin.*
import com.github.shadowsocks.preference.*
import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.readableMessage
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.parcel.Parcelize
class ProfileConfigFragment : PreferenceFragmentCompat(),
Preference.OnPreferenceChangeListener, OnPreferenceDataStoreChangeListener {
......@@ -46,6 +47,20 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
override fun provideSummary(preference: EditTextPreference?) = "\u2022".repeat(preference?.text?.length ?: 0)
private const val REQUEST_CODE_PLUGIN_CONFIGURE = 1
const val REQUEST_UNSAVED_CHANGES = 2
}
@Parcelize
data class ProfileIdArg(val profileId: Long) : Parcelable
class DeleteConfirmationDialogFragment : AlertDialogFragment<ProfileIdArg, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setTitle(R.string.delete_confirm_prompt)
setPositiveButton(R.string.yes) { _, _ ->
ProfileManager.delProfile(arg.profileId)
requireActivity().finish()
}
setNegativeButton(R.string.no, null)
}
}
private var profileId = -1L
......@@ -114,7 +129,7 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
}.show(fragmentManager ?: return, Key.pluginConfigure)
}
fun saveAndExit() {
private fun saveAndExit() {
val profile = ProfileManager.getProfile(profileId) ?: Profile()
profile.id = profileId
profile.deserialize()
......@@ -140,7 +155,7 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
DataStore.dirty = true
true
} catch (exc: RuntimeException) {
Snackbar.make(view!!, exc.localizedMessage, Snackbar.LENGTH_LONG).show()
Snackbar.make(view!!, exc.readableMessage, Snackbar.LENGTH_LONG).show()
false
}
......@@ -167,28 +182,26 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_PLUGIN_CONFIGURE) when (resultCode) {
when (requestCode) {
REQUEST_CODE_PLUGIN_CONFIGURE -> when (resultCode) {
Activity.RESULT_OK -> {
val options = data?.getStringExtra(PluginContract.EXTRA_OPTIONS)
pluginConfigure.text = options
onPreferenceChange(null, options)
}
PluginContract.RESULT_FALLBACK -> showPluginEditor()
} else super.onActivityResult(requestCode, resultCode, data)
}
REQUEST_UNSAVED_CHANGES -> when (resultCode) {
DialogInterface.BUTTON_POSITIVE -> saveAndExit()
DialogInterface.BUTTON_NEGATIVE -> requireActivity().finish()
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_delete -> {
val activity = requireActivity()
AlertDialog.Builder(activity)
.setTitle(R.string.delete_confirm_prompt)
.setPositiveButton(R.string.yes) { _, _ ->
ProfileManager.delProfile(profileId)
activity.finish()
}
.setNegativeButton(R.string.no, null)
.create()
.show()
DeleteConfirmationDialogFragment().withArg(ProfileIdArg(profileId)).show(this)
true
}
R.id.action_apply -> {
......
......@@ -49,12 +49,12 @@ import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.datas
import com.github.shadowsocks.utils.printLog
import com.github.shadowsocks.utils.readableMessage
import com.github.shadowsocks.widget.UndoSnackbarManager
import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.AdSize
import com.google.android.gms.ads.AdView
import net.glxn.qrgen.android.QRCode
import org.json.JSONArray
class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
companion object {
......@@ -65,21 +65,16 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
private const val KEY_URL = "com.github.shadowsocks.QRCodeDialog.KEY_URL"
private const val REQUEST_IMPORT_PROFILES = 1
private const val REQUEST_REPLACE_PROFILES = 3
private const val REQUEST_EXPORT_PROFILES = 2
}
/**
* Is ProfilesFragment editable at all.
*/
private val isEnabled get() = when ((activity as MainActivity).state) {
BaseService.CONNECTED, BaseService.STOPPED -> true
else -> false
}
private fun isProfileEditable(id: Long) = when ((activity as MainActivity).state) {
BaseService.CONNECTED -> id !in Core.activeProfileIds
BaseService.STOPPED -> true
else -> false
}
private val isEnabled get() = (activity as MainActivity).state.let { it.canStop || it == BaseService.State.Stopped }
private fun isProfileEditable(id: Long) =
(activity as MainActivity).state == BaseService.State.Stopped || id !in Core.activeProfileIds
@SuppressLint("ValidFragment")
class QRCodeDialog() : DialogFragment() {
......@@ -175,21 +170,20 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
var adView = adView
if (item.host == "198.199.101.152") {
if (adView == null) {
val params = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
val params = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
AdSize.SMART_BANNER.getHeightInPixels(context))
params.gravity = Gravity.CENTER_HORIZONTAL
adView = AdView(context)
adView.layoutParams = params
adView.adUnitId = "ca-app-pub-9097031975646651/7760346322"
adView.adSize = AdSize.FLUID
val padding = context.resources.getDimensionPixelOffset(R.dimen.profile_padding)
adView.setPadding(padding, 0, 0, padding)
adView.adSize = AdSize.SMART_BANNER
itemView.findViewById<LinearLayout>(R.id.content).addView(adView)
// Load Ad
val adBuilder = AdRequest.Builder()
adBuilder.addTestDevice("B08FC1764A7B250E91EA9D0D5EBEB208")
adBuilder.addTestDevice("7509D18EB8AF82F915874FEF53877A64")
adView.loadAd(adBuilder.build())
this.adView = adView
} else adView.visibility = View.VISIBLE
......@@ -203,7 +197,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
Core.switchProfile(item.id)
profilesAdapter.refreshId(old)
itemView.isSelected = true
if (activity.state == BaseService.CONNECTED) Core.reloadService()
if (activity.state.canStop) Core.reloadService()
}
}
......@@ -296,6 +290,11 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
notifyItemRemoved(index)
if (profileId == DataStore.profileId) DataStore.profileId = 0 // switch to null profile
}
override fun onCleared() {
profiles.clear()
notifyDataSetChanged()
}
}
private var selectedItem: ProfileViewHolder? = null
......@@ -383,12 +382,20 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
}
R.id.action_import_file -> {
startFilesForResult(Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/json"
type = "application/*"
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/*", "text/*"))
}, REQUEST_IMPORT_PROFILES)
true
}
R.id.action_replace_file -> {
startFilesForResult(Intent(Intent.ACTION_GET_CONTENT).apply {
type = "application/*"
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/*", "text/*"))
}, REQUEST_REPLACE_PROFILES)
true
}
R.id.action_manual_settings -> {
startConfig(ProfileManager.createProfile(
Profile().also { Core.currentProfile?.first?.copyFeatureSettingsTo(it) }))
......@@ -404,7 +411,6 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
}
R.id.action_export_file -> {
startFilesForResult(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/json"
putExtra(Intent.EXTRA_TITLE, "profiles.json") // optional title that can be edited
}, REQUEST_EXPORT_PROFILES)
......@@ -414,9 +420,9 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
}
}
private fun startFilesForResult(intent: Intent?, requestCode: Int) {
private fun startFilesForResult(intent: Intent, requestCode: Int) {
try {
startActivityForResult(intent, requestCode)
startActivityForResult(intent.addCategory(Intent.CATEGORY_OPENABLE), requestCode)
return
} catch (_: ActivityNotFoundException) { } catch (_: SecurityException) { }
(activity as MainActivity).snackbar(getString(R.string.file_manager_missing)).show()
......@@ -426,31 +432,34 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
if (resultCode != Activity.RESULT_OK) super.onActivityResult(requestCode, resultCode, data)
else when (requestCode) {
REQUEST_IMPORT_PROFILES -> {
val feature = Core.currentProfile?.first
var success = false
val activity = activity as MainActivity
for (uri in data!!.datas) try {
Profile.parseJson(activity.contentResolver.openInputStream(uri)!!.bufferedReader().readText(),
feature) {
ProfileManager.createProfile(it)
success = true
try {
ProfileManager.createProfilesFromJson(data!!.datas.asSequence().map {
activity.contentResolver.openInputStream(it)
})
} catch (e: Exception) {
activity.snackbar(e.readableMessage).show()
}
}
REQUEST_REPLACE_PROFILES -> {
val activity = activity as MainActivity
try {
ProfileManager.createProfilesFromJson(data!!.datas.asSequence().map {
activity.contentResolver.openInputStream(it)
}, true)
} catch (e: Exception) {
printLog(e)
activity.snackbar(e.readableMessage).show()
}
activity.snackbar().setText(if (success) R.string.action_import_msg else R.string.action_import_err)
.show()
}
REQUEST_EXPORT_PROFILES -> {
val profiles = ProfileManager.getAllProfiles()
val profiles = ProfileManager.serializeToJson()
if (profiles != null) try {
val lookup = LongSparseArray<Profile>(profiles.size).apply { profiles.forEach { put(it.id, it) } }
requireContext().contentResolver.openOutputStream(data?.data!!)!!.bufferedWriter().use {
it.write(JSONArray(profiles.map { it.toJson(lookup) }.toTypedArray()).toString(2))
it.write(profiles.toString(2))
}
} catch (e: Exception) {
printLog(e)
(activity as MainActivity).snackbar(e.localizedMessage).show()
(activity as MainActivity).snackbar(e.readableMessage).show()
}
}
else -> super.onActivityResult(requestCode, resultCode, data)
......
......@@ -55,14 +55,15 @@ class QuickToggleShortcut : Activity(), ShadowsocksConnection.Callback {
}
override fun onServiceConnected(service: IShadowsocksService) {
when (service.state) {
BaseService.STOPPED -> Core.startService()
BaseService.CONNECTED -> Core.stopService()
val state = BaseService.State.values()[service.state]
when {
state.canStop -> Core.stopService()
state == BaseService.State.Stopped -> Core.startService()
}
finish()
}
override fun stateChanged(state: Int, profileName: String?, msg: String?) { }
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) { }
override fun onDestroy() {
connection.disconnect(this)
......
......@@ -22,9 +22,12 @@ package com.github.shadowsocks.acl
import android.content.ClipData
import android.content.ClipboardManager
import android.content.DialogInterface
import android.content.Intent
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.text.Editable
import android.text.TextWatcher
import android.view.*
......@@ -43,10 +46,13 @@ import com.github.shadowsocks.R
import com.github.shadowsocks.ToolbarFragment
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.net.Subnet
import com.github.shadowsocks.plugin.AlertDialogFragment
import com.github.shadowsocks.utils.asIterable
import com.github.shadowsocks.utils.readableMessage
import com.github.shadowsocks.utils.resolveResourceId
import com.github.shadowsocks.widget.UndoSnackbarManager
import com.google.android.material.textfield.TextInputLayout
import kotlinx.android.parcel.Parcelize
import java.net.IDN
import java.net.MalformedURLException
import java.net.URL
......@@ -55,6 +61,8 @@ import java.util.regex.PatternSyntaxException
class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, ActionMode.Callback {
companion object {
private const val REQUEST_CODE_ADD = 1
private const val REQUEST_CODE_EDIT = 2
private const val TEMPLATE_REGEX_DOMAIN = "(^|\\.)%s$"
private const val SELECTED_SUBNETS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_SUBNETS"
......@@ -63,6 +71,14 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
// unescaped: (?<=^(\(\^\|\\\.\)|\^\(\.\*\\\.\)\?)).*(?=\$$)
private val domainPattern = "(?<=^(\\(\\^\\|\\\\\\.\\)|\\^\\(\\.\\*\\\\\\.\\)\\?)).*(?=\\\$\$)".toRegex()
@Suppress("FunctionName")
private fun AclItem(item: Any) = when (item) {
is String -> AclItem(item, false)
is Subnet -> AclItem(item.toString(), false)
is URL -> AclItem(item.toString(), true)
else -> throw IllegalArgumentException("item")
}
}
private enum class Template {
......@@ -70,53 +86,50 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
Domain,
Url;
}
private inner class AclRuleDialog(item: Any = "") : TextWatcher, AdapterView.OnItemSelectedListener {
val builder: AlertDialog.Builder
val templateSelector: Spinner
val editText: EditText
private val inputLayout: TextInputLayout
private lateinit var dialog: AlertDialog
@Parcelize
data class AclItem(val item: String = "", val isUrl: Boolean = false) : Parcelable {
fun toAny() = if (isUrl) URL(item) else Subnet.fromString(item) ?: item
}
@Parcelize
data class AclEditResult(val edited: AclItem, val replacing: AclItem) : Parcelable
class AclRuleDialogFragment : AlertDialogFragment<AclItem, AclEditResult>(),
TextWatcher, AdapterView.OnItemSelectedListener {
private lateinit var templateSelector: Spinner
private lateinit var editText: EditText
private lateinit var inputLayout: TextInputLayout
private lateinit var positive: Button
init {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
val activity = requireActivity()
val view = activity.layoutInflater.inflate(R.layout.dialog_acl_rule, null)
templateSelector = view.findViewById(R.id.template_selector)
editText = view.findViewById(R.id.content)
inputLayout = view.findViewById(R.id.content_layout)
when (item) {
is String -> {
val match = domainPattern.find(item)
templateSelector.setSelection(Template.Generic.ordinal)
editText.setText(arg.item)
when {
arg.isUrl -> templateSelector.setSelection(Template.Url.ordinal)
Subnet.fromString(arg.item) == null -> {
val match = domainPattern.find(arg.item)
if (match != null) {
templateSelector.setSelection(Template.Domain.ordinal)
editText.setText(IDN.toUnicode(match.value.replace("\\.", "."),
IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES))
} else {
templateSelector.setSelection(Template.Generic.ordinal)
editText.setText(item)
}
}
is URL -> {
templateSelector.setSelection(Template.Url.ordinal)
editText.setText(item.toString())
}
else -> {
templateSelector.setSelection(Template.Generic.ordinal)
editText.setText(item.toString())
}
}
templateSelector.onItemSelectedListener = this
editText.addTextChangedListener(this)
builder = AlertDialog.Builder(activity)
.setTitle(R.string.edit_rule)
.setNegativeButton(android.R.string.cancel, null)
.setView(view)
templateSelector.onItemSelectedListener = this@AclRuleDialogFragment
editText.addTextChangedListener(this@AclRuleDialogFragment)
setTitle(R.string.edit_rule)
setPositiveButton(android.R.string.ok, listener)
setNegativeButton(android.R.string.cancel, null)
if (arg.item.isNotEmpty()) setNeutralButton(R.string.delete, listener)
setView(view)
}
fun show() {
dialog = builder.create()
dialog.show()
positive = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
override fun onStart() {
super.onStart()
positive = (dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)
validate()
}
......@@ -134,7 +147,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
if (Subnet.fromString(this) == null) toPattern()
true
} catch (e: PatternSyntaxException) {
message = e.localizedMessage
message = e.readableMessage
false
}
}
......@@ -142,7 +155,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
IDN.toASCII(value.toString(), IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES)
true
} catch (e: IllegalArgumentException) {
message = e.cause?.localizedMessage ?: e.localizedMessage
message = e.cause?.readableMessage ?: e.readableMessage
false
}
Template.Url -> try {
......@@ -150,22 +163,21 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
if ("http".equals(url.protocol, true)) message = getString(R.string.cleartext_http_warning)
true
} catch (e: MalformedURLException) {
message = e.localizedMessage
message = e.readableMessage
false
}
}
inputLayout.error = message
}
fun add(): Int? {
val text = editText.text.toString()
return when (Template.values()[templateSelector.selectedItemPosition]) {
Template.Generic -> adapter.addToProxy(text)
Template.Domain -> adapter.addHostname(TEMPLATE_REGEX_DOMAIN.format(Locale.ENGLISH, IDN.toASCII(text,
override val ret get() = AclEditResult(editText.text.toString().let { text ->
when (Template.values()[templateSelector.selectedItemPosition]) {
Template.Generic -> AclItem(text)
Template.Domain -> AclItem(TEMPLATE_REGEX_DOMAIN.format(Locale.ENGLISH, IDN.toASCII(text,
IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES).replace(".", "\\.")))
Template.Url -> adapter.addURL(URL(text))
}
Template.Url -> AclItem(text, true)
}
}, arg)
}
private inner class AclRuleViewHolder(view: View) : RecyclerView.ViewHolder(view),
......@@ -197,22 +209,10 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
}
override fun onClick(v: View?) {
if (selectedItems.isNotEmpty()) onLongClick(v) else {
val dialog = AclRuleDialog(item)
dialog.builder
.setNeutralButton(R.string.delete) { _, _ ->
adapter.remove(item)
undoManager.remove(Pair(-1, item))
if (selectedItems.isNotEmpty()) onLongClick(v)
else AclRuleDialogFragment().withArg(AclItem(item)).show(this@CustomRulesFragment, REQUEST_CODE_EDIT)
}
.setPositiveButton(android.R.string.ok) { _, _ ->
adapter.remove(item)
val index = dialog.add() ?: adapter.add(item)
if (index != null) list.post { list.scrollToPosition(index) }
}
dialog.show()
}
}
override fun onLongClick(p0: View?): Boolean {
override fun onLongClick(v: View?): Boolean {
if (!selectedItems.add(item)) selectedItems.remove(item) // toggle
onSelectedItemsUpdated()
itemView.isSelected = !itemView.isSelected
......@@ -346,11 +346,8 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
}
}
private val isEnabled get() = when ((activity as MainActivity).state) {
BaseService.CONNECTED -> Core.currentProfile?.first?.route != Acl.CUSTOM_RULES
BaseService.STOPPED -> true
else -> false
}
private val isEnabled get() = (activity as MainActivity).state == BaseService.State.Stopped ||
Core.currentProfile?.first?.route != Acl.CUSTOM_RULES
private val selectedItems = HashSet<Any>()
private val adapter by lazy { AclRulesAdapter() }
......@@ -427,9 +424,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) {
R.id.action_manual_settings -> {
val dialog = AclRuleDialog()
dialog.builder.setPositiveButton(android.R.string.ok) { _, _ -> dialog.add() }
dialog.show()
AclRuleDialogFragment().withArg(AclItem()).show(this, REQUEST_CODE_ADD)
true
}
R.id.action_import_clipboard -> {
......@@ -451,6 +446,25 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
else -> false
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val editing = when (requestCode) {
REQUEST_CODE_ADD -> false
REQUEST_CODE_EDIT -> true
else -> return super.onActivityResult(requestCode, resultCode, data)
}
val ret by lazy { AlertDialogFragment.getRet<AclEditResult>(data!!) }
when (resultCode) {
DialogInterface.BUTTON_POSITIVE -> {
if (editing) adapter.remove(ret.replacing.toAny())
adapter.add(ret.edited.toAny())?.also { list.post { list.scrollToPosition(it) } }
}
DialogInterface.BUTTON_NEUTRAL -> ret.replacing.toAny().let { item ->
adapter.remove(item)
undoManager.remove(Pair(-1, item))
}
}
}
override fun onDetach() {
undoManager.flush()
mode?.finish()
......
......@@ -41,9 +41,9 @@ class TileService : BaseTileService(), ShadowsocksConnection.Callback {
private var tapPending = false
private val connection = ShadowsocksConnection()
override fun stateChanged(state: Int, profileName: String?, msg: String?) = updateTile(state) { profileName }
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) = updateTile(state) { profileName }
override fun onServiceConnected(service: IShadowsocksService) {
updateTile(service.state) { service.profileName }
updateTile(BaseService.State.values()[service.state]) { service.profileName }
if (tapPending) {
tapPending = false
onClick()
......@@ -63,23 +63,28 @@ class TileService : BaseTileService(), ShadowsocksConnection.Callback {
if (isLocked && !DataStore.canToggleLocked) unlockAndRun(this::toggle) else toggle()
}
private fun updateTile(serviceState: Int, profileName: () -> String?) {
private fun updateTile(serviceState: BaseService.State, profileName: () -> String?) {
qsTile?.apply {
label = null
when (serviceState) {
BaseService.STOPPED -> {
icon = iconIdle
state = Tile.STATE_INACTIVE
BaseService.State.Idle -> throw IllegalStateException("serviceState")
BaseService.State.Connecting -> {
icon = iconBusy
state = Tile.STATE_ACTIVE
}
BaseService.CONNECTED -> {
BaseService.State.Connected -> {
icon = iconConnected
if (!keyguard.isDeviceLocked) label = profileName()
state = Tile.STATE_ACTIVE
}
else -> {
BaseService.State.Stopping -> {
icon = iconBusy
state = Tile.STATE_UNAVAILABLE
}
BaseService.State.Stopped -> {
icon = iconIdle
state = Tile.STATE_INACTIVE
}
}
label = label ?: getString(R.string.app_name)
updateTile()
......@@ -88,9 +93,11 @@ class TileService : BaseTileService(), ShadowsocksConnection.Callback {
private fun toggle() {
val service = connection.service
if (service == null) tapPending = true else when (service.state) {
BaseService.STOPPED -> Core.startService()
BaseService.CONNECTED -> Core.stopService()
if (service == null) tapPending = true else BaseService.State.values()[service.state].let { state ->
when {
state.canStop -> Core.stopService()
state == BaseService.State.Stopped -> Core.startService()
}
}
}
}
......@@ -68,14 +68,14 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute
return drawableState
}
fun changeState(state: Int, animate: Boolean) {
fun changeState(state: BaseService.State, animate: Boolean) {
when (state) {
BaseService.CONNECTING -> changeState(iconConnecting, animate)
BaseService.CONNECTED -> changeState(iconConnected, animate)
BaseService.STOPPING -> changeState(iconStopping, animate)
BaseService.State.Connecting -> changeState(iconConnecting, animate)
BaseService.State.Connected -> changeState(iconConnected, animate)
BaseService.State.Stopping -> changeState(iconStopping, animate)
else -> changeState(iconStopped, animate)
}
if (state == BaseService.CONNECTED) {
if (state == BaseService.State.Connected) {
checked = true
TooltipCompat.setTooltipText(this, context.getString(R.string.stop))
} else {
......@@ -83,7 +83,7 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute
TooltipCompat.setTooltipText(this, context.getString(R.string.connect))
}
refreshDrawableState()
isEnabled = state == BaseService.CONNECTED || state == BaseService.STOPPED
isEnabled = state.canStop || state == BaseService.State.Stopped
}
private fun counters(a: AnimatedVectorDrawableCompat, b: AnimatedVectorDrawableCompat): Boolean =
......
......@@ -65,15 +65,15 @@ class StatsBar @JvmOverloads constructor(context: Context, attrs: AttributeSet?
super.setOnClickListener(l)
}
fun changeState(state: Int) {
fun changeState(state: BaseService.State) {
val activity = context as MainActivity
if (state != BaseService.CONNECTED) {
if (state != BaseService.State.Connected) {
updateTraffic(0, 0, 0, 0)
tester.status.removeObservers(activity)
if (state != BaseService.IDLE) tester.invalidate()
if (state != BaseService.State.Idle) tester.invalidate()
statusText.setText(when (state) {
BaseService.CONNECTING -> R.string.connecting
BaseService.STOPPING -> R.string.stopping
BaseService.State.Connecting -> R.string.connecting
BaseService.State.Stopping -> R.string.stopping
else -> R.string.not_connected
})
} else {
......
......@@ -18,6 +18,11 @@
android:id="@+id/action_import_file"
android:alphabeticShortcut="o"
android:title="@string/action_import_file"/>
<item
android:id="@+id/action_replace_file"
android:alphabeticShortcut="o"
android:title="@string/action_replace_file"
app:alphabeticModifiers="CTRL|SHIFT"/>
<item
android:id="@+id/action_manual_settings"
android:title="@string/add_profile_methods_manual_settings"
......
* 1.2.0:
* New helper class `AlertDialogFragment` for creating `AlertDialog` that persists through configuration changes;
* Dependency update: `com.google.android.material:material:1.1.0-alpha03`.
* 1.1.0:
* Having control characters in plugin options is no longer allowed.
If this breaks your plugin, you are doing it wrong.
......
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'com.vanniktech.maven.publish'
apply from: 'gradle-mvn-push.gradle'
@SuppressWarnings("GroovyUnusedDeclaration")
def getRepositoryUsername() {
return hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : ""
}
@SuppressWarnings("GroovyUnusedDeclaration")
def getRepositoryPassword() {
return hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : ""
}
android {
buildToolsVersion rootProject.buildToolsVersion
......@@ -10,10 +20,10 @@ android {
defaultConfig {
minSdkVersion rootProject.minSdkVersion
targetSdkVersion rootProject.sdkVersion
versionCode 8
versionName "1.1.0"
versionCode Integer.parseInt(VERSION_CODE)
versionName VERSION_NAME
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunner "androidx.testgetRepositoryPassword().runner.AndroidJUnitRunner"
}
buildTypes {
......@@ -23,6 +33,21 @@ android {
}
}
androidExtensions {
experimental = true
}
mavenPublish {
targets {
uploadArchives {
releaseRepositoryUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
snapshotRepositoryUrl = "https://oss.sonatype.org/content/repositories/snapshots/"
repositoryUsername = getRepositoryUsername()
repositoryPassword = getRepositoryPassword()
}
}
}
dependencies {
api 'androidx.core:core-ktx:1.0.1'
api 'com.google.android.material:material:1.1.0-alpha03'
......
/*
* Copyright 2013 Chris Banes
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
apply plugin: 'maven'
apply plugin: 'signing'
@SuppressWarnings(["GroovyUnusedDeclaration", "GrMethodMayBeStatic"])
def isReleaseBuild() {
return !android.defaultConfig.versionName.contains("SNAPSHOT")
}
@SuppressWarnings("GroovyUnusedDeclaration")
def getReleaseRepositoryUrl() {
return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL
: "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
}
@SuppressWarnings("GroovyUnusedDeclaration")
def getSnapshotRepositoryUrl() {
return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL
: "https://oss.sonatype.org/content/repositories/snapshots/"
}
@SuppressWarnings("GroovyUnusedDeclaration")
def getRepositoryUsername() {
return hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : ""
}
@SuppressWarnings("GroovyUnusedDeclaration")
def getRepositoryPassword() {
return hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : ""
}
afterEvaluate { project ->
uploadArchives {
repositories {
mavenDeployer {
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
pom.groupId = 'com.github.shadowsocks'
pom.artifactId = 'plugin'
pom.version = android.defaultConfig.versionName
repository(url: getReleaseRepositoryUrl()) {
authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
}
snapshotRepository(url: getSnapshotRepositoryUrl()) {
authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
}
pom.project {
name 'Shadowsocks Plugin'
packaging 'aar'
description 'Shadowsocks Plugin'
url 'https://github.com/shadowsocks/shadowsocks-android'
scm {
url 'https://github.com/shadowsocks/shadowsocks-android.git'
connection 'scm:git:git://github.com/shadowsocks/shadowsocks-android.git'
developerConnection 'scm:git:git@github.com:shadowsocks/shadowsocks-android.git'
}
licenses {
license {
name 'GPLv3'
url 'https://www.gnu.org/licenses/gpl-3.0.html'
distribution 'repo'
}
}
developers {
developer {
id 'madeye'
name 'Max Lv'
url 'https://github.com/madeye'
}
developer {
id 'Mygod'
name 'Mygod Studio'
url 'https://github.com/Mygod'
}
}
}
}
}
}
signing {
required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
sign configurations.archives
}
android.libraryVariants.all { variant ->
def javadocTask = task("generate${variant.name.capitalize()}Javadoc", type: Javadoc) {
description "Generates Javadoc for $variant.name."
source = android.sourceSets.main.java.srcDirs
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
exclude '**/*.kt'
exclude '**/BuildConfig.java'
exclude '**/R.java'
}
def jarJavadocTask = task("jar${variant.name.capitalize()}Javadoc", type: Jar) {
description "Generate Javadoc Jar for $variant.name"
classifier = 'javadoc'
from javadocTask.destinationDir
}
jarJavadocTask.dependsOn javadocTask
artifacts.add('archives', jarJavadocTask)
def jarSourceTask = task("jar${variant.name.capitalize()}Sources", type: Jar) {
description "Generates Java Sources for $variant.name."
classifier = 'sources'
from android.sourceSets.main.java.srcDirs
}
artifacts.add('archives', jarSourceTask)
}
}
GROUP=com.github.shadowsocks
VERSION_NAME=1.2.0
VERSION_CODE=9
POM_ARTIFACT_ID=plugin
POM_NAME=Shadowsocks Plugin
POM_PACKAGING=aar
POM_DESCRIPTION=SIP003 plugin for Shadowsocks
POM_INCEPTION_YEAR=2018
POM_URL=https://github.com/shadowsocks/shadowsocks-android
POM_SCM_URL=https://github.com/shadowsocks/shadowsocks-android
POM_SCM_CONNECTION=scm:git:git://github.com/shadowsocks/shadowsocks-android.git
POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/shadowsocks/shadowsocks-android.git
POM_LICENCE_NAME=The GNU General Public License v3.0
POM_LICENCE_URL=https://www.gnu.org/licenses/gpl-3.0.html
POM_LICENCE_DIST=repo
POM_DEVELOPER_ID=Mygod
POM_DEVELOPER_NAME=Mygod Studio
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.plugin
import android.app.Activity
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.fragment.app.Fragment
import kotlinx.android.parcel.Parcelize
/**
* Based on: https://android.googlesource.com/platform/packages/apps/ExactCalculator/+/8c43f06/src/com/android/calculator2/AlertDialogFragment.java
*/
abstract class AlertDialogFragment<Arg : Parcelable, Ret : Parcelable> :
AppCompatDialogFragment(), DialogInterface.OnClickListener {
companion object {
private const val KEY_ARG = "arg"
private const val KEY_RET = "ret"
fun <T : Parcelable> getRet(data: Intent) = data.extras!!.getParcelable<T>(KEY_RET)!!
}
protected abstract fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener)
protected val arg by lazy { arguments!!.getParcelable<Arg>(KEY_ARG)!! }
protected open val ret: Ret? get() = null
fun withArg(arg: Arg) = apply { arguments = Bundle().apply { putParcelable(KEY_ARG, arg) } }
override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog =
AlertDialog.Builder(requireContext()).also { it.prepare(this) }.create()
override fun onClick(dialog: DialogInterface?, which: Int) {
targetFragment?.onActivityResult(targetRequestCode, which, ret?.let {
Intent().replaceExtras(Bundle().apply { putParcelable(KEY_RET, it) })
})
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
targetFragment?.onActivityResult(targetRequestCode, Activity.RESULT_CANCELED, null)
}
fun show(target: Fragment, requestCode: Int = 0, tag: String = javaClass.simpleName) {
setTargetFragment(target, requestCode)
show(target.fragmentManager ?: return, tag)
}
}
@Parcelize
class Empty : Parcelable
......@@ -38,7 +38,7 @@ class MainFragment : LeanbackSettingsFragmentCompat() {
override fun onPreferenceDisplayDialog(caller: PreferenceFragmentCompat, pref: Preference?): Boolean {
if (pref?.key == Key.id) {
if ((childFragmentManager.findFragmentById(R.id.settings_preference_fragment_container)
as MainPreferenceFragment).state == BaseService.STOPPED) {
as MainPreferenceFragment).state == BaseService.State.Stopped) {
startPreferenceFragment(ProfilesDialogFragment().apply {
arguments = bundleOf(Pair(LeanbackPreferenceDialogFragmentCompat.ARG_KEY, Key.id))
setTargetFragment(caller, 0)
......
......@@ -30,7 +30,6 @@ import android.os.DeadObjectException
import android.os.Handler
import android.text.format.Formatter
import android.util.Log
import android.util.LongSparseArray
import android.widget.Toast
import androidx.leanback.preference.LeanbackPreferenceFragmentCompat
import androidx.lifecycle.Observer
......@@ -44,7 +43,6 @@ import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.ShadowsocksConnection
import com.github.shadowsocks.aidl.TrafficStats
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.net.HttpsTest
import com.github.shadowsocks.net.TcpFastOpen
......@@ -54,13 +52,13 @@ import com.github.shadowsocks.preference.PortPreferenceListener
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.datas
import com.github.shadowsocks.utils.printLog
import org.json.JSONArray
import com.github.shadowsocks.utils.readableMessage
class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksConnection.Callback,
OnPreferenceDataStoreChangeListener {
companion object {
private const val REQUEST_CONNECT = 1
private const val REQUEST_IMPORT_PROFILES = 2
private const val REQUEST_REPLACE_PROFILES = 2
private const val REQUEST_EXPORT_PROFILES = 3
private const val TAG = "MainPreferenceFragment"
}
......@@ -88,9 +86,9 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
private lateinit var tester: HttpsTest
// service
var state = BaseService.IDLE
var state = BaseService.State.Idle
private set
override fun stateChanged(state: Int, profileName: String?, msg: String?) = changeState(state, msg)
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) = changeState(state, msg)
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
if (profileId == 0L) requireContext().let { context ->
this.stats.summary = getString(R.string.stat_summary,
......@@ -101,26 +99,26 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
}
}
private fun changeState(state: Int, msg: String? = null) {
fab.isEnabled = state == BaseService.STOPPED || state == BaseService.CONNECTED
private fun changeState(state: BaseService.State, msg: String? = null) {
fab.isEnabled = state.canStop || state == BaseService.State.Stopped
fab.setTitle(when (state) {
BaseService.CONNECTING -> R.string.connecting
BaseService.CONNECTED -> R.string.stop
BaseService.STOPPING -> R.string.stopping
BaseService.State.Connecting -> R.string.connecting
BaseService.State.Connected -> R.string.stop
BaseService.State.Stopping -> R.string.stopping
else -> R.string.connect
})
stats.setTitle(R.string.connection_test_pending)
stats.isVisible = state == BaseService.CONNECTED
if (state != BaseService.CONNECTED) {
stats.isVisible = state == BaseService.State.Connected
if (state != BaseService.State.Connected) {
trafficUpdated(0, TrafficStats())
tester.status.removeObservers(this)
if (state != BaseService.IDLE) tester.invalidate()
if (state != BaseService.State.Idle) tester.invalidate()
} else tester.status.observe(this, Observer {
it.retrieve(stats::setTitle) { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
})
if (msg != null) Toast.makeText(requireContext(), getString(R.string.vpn_error, msg), Toast.LENGTH_SHORT).show()
this.state = state
if (state == BaseService.STOPPED) {
if (state == BaseService.State.Stopped) {
controlImport.isEnabled = true
tfo.isEnabled = true
serviceMode.isEnabled = true
......@@ -141,11 +139,11 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
private val handler = Handler()
private val connection = ShadowsocksConnection(handler, true)
override fun onServiceConnected(service: IShadowsocksService) = changeState(try {
service.state
BaseService.State.values()[service.state]
} catch (_: DeadObjectException) {
BaseService.IDLE
BaseService.State.Idle
})
override fun onServiceDisconnected() = changeState(BaseService.IDLE)
override fun onServiceDisconnected() = changeState(BaseService.State.Idle)
override fun onBinderDied() {
connection.disconnect(requireContext())
connection.connect(requireContext(), this)
......@@ -203,7 +201,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
}
tester = ViewModelProviders.of(this).get()
changeState(BaseService.IDLE) // reset everything to init state
changeState(BaseService.State.Idle) // reset everything to init state
connection.connect(requireContext(), this)
DataStore.publicStore.registerChangeListener(this)
}
......@@ -228,7 +226,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
fun startService() {
when {
state != BaseService.STOPPED -> return
state != BaseService.State.Stopped -> return
DataStore.serviceMode == Key.modeVpn -> {
val intent = VpnService.prepare(requireContext())
if (intent != null) startActivityForResult(intent, REQUEST_CONNECT)
......@@ -254,7 +252,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
override fun onPreferenceTreeClick(preference: Preference?) = when (preference?.key) {
Key.id -> {
if (state == BaseService.CONNECTED) Core.stopService()
if (state == BaseService.State.Connected) Core.stopService()
true
}
Key.controlStats -> {
......@@ -263,15 +261,14 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
}
Key.controlImport -> {
startFilesForResult(Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/json"
type = "application/*"
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}, REQUEST_IMPORT_PROFILES)
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/*", "text/*"))
}, REQUEST_REPLACE_PROFILES)
true
}
Key.controlExport -> {
startFilesForResult(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/json"
putExtra(Intent.EXTRA_TITLE, "profiles.json") // optional title that can be edited
}, REQUEST_EXPORT_PROFILES)
......@@ -280,9 +277,9 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
else -> super.onPreferenceTreeClick(preference)
}
private fun startFilesForResult(intent: Intent?, requestCode: Int) {
private fun startFilesForResult(intent: Intent, requestCode: Int) {
try {
startActivityForResult(intent, requestCode)
startActivityForResult(intent.addCategory(Intent.CATEGORY_OPENABLE), requestCode)
return
} catch (_: ActivityNotFoundException) { } catch (_: SecurityException) { }
Toast.makeText(requireContext(), R.string.file_manager_missing, Toast.LENGTH_SHORT).show()
......@@ -294,41 +291,30 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
Toast.makeText(requireContext(), R.string.vpn_permission_denied, Toast.LENGTH_SHORT).show()
Crashlytics.log(Log.ERROR, TAG, "Failed to start VpnService from onActivityResult: $data")
}
REQUEST_IMPORT_PROFILES -> {
REQUEST_REPLACE_PROFILES -> {
if (resultCode != Activity.RESULT_OK) return
val profiles = ProfileManager.getAllProfiles()?.associateBy { it.formattedAddress }
val feature = profiles?.values?.singleOrNull { it.id == DataStore.profileId }
val lazyClear = lazy { ProfileManager.clear() }
val context = requireContext()
for (uri in data!!.datas) try {
Profile.parseJson(context.contentResolver.openInputStream(uri)!!.bufferedReader().readText(),
feature) {
lazyClear.value
// if two profiles has the same address, treat them as the same profile and copy stats over
profiles?.get(it.formattedAddress)?.apply {
it.tx = tx
it.rx = rx
}
ProfileManager.createProfile(it)
}
try {
ProfileManager.createProfilesFromJson(data!!.datas.asSequence().map {
context.contentResolver.openInputStream(it)
}, true)
} catch (e: Exception) {
printLog(e)
Toast.makeText(context, e.localizedMessage, Toast.LENGTH_SHORT).show()
Toast.makeText(context, e.readableMessage, Toast.LENGTH_SHORT).show()
}
populateProfiles()
}
REQUEST_EXPORT_PROFILES -> {
if (resultCode != Activity.RESULT_OK) return
val profiles = ProfileManager.getAllProfiles()
val profiles = ProfileManager.serializeToJson()
val context = requireContext()
if (profiles != null) try {
val lookup = LongSparseArray<Profile>(profiles.size).apply { profiles.forEach { put(it.id, it) } }
context.contentResolver.openOutputStream(data?.data!!)!!.bufferedWriter().use {
it.write(JSONArray(profiles.map { it.toJson(lookup) }.toTypedArray()).toString(2))
it.write(profiles.toString(2))
}
} catch (e: Exception) {
printLog(e)
Toast.makeText(context, e.localizedMessage, Toast.LENGTH_SHORT).show()
Toast.makeText(context, e.readableMessage, Toast.LENGTH_SHORT).show()
}
}
else -> super.onActivityResult(requestCode, resultCode, data)
......
......@@ -13,7 +13,7 @@
app:summary="@string/stat_summary"/>
<Preference
app:key="control.import"
app:title="@string/action_import_file"/>
app:title="@string/action_replace_file"/>
<Preference
app:key="control.export"
app:title="@string/action_export_file"/>
......
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