Commit f9500370 authored by Max Lv's avatar Max Lv Committed by GitHub

Merge pull request #1258 from shadowsocks/o-preview

Support for Android O preview
parents 2b985686 d8179b5b
...@@ -5,7 +5,7 @@ jdk: ...@@ -5,7 +5,7 @@ jdk:
env: env:
global: global:
- NDK_VERSION=r14 - NDK_VERSION=r15b
- NDK_CCACHE=ccache - NDK_CCACHE=ccache
- GOROOT_BOOTSTRAP=$GOROOT - GOROOT_BOOTSTRAP=$GOROOT
- ANDROID_NDK_HOME=$HOME/.android/android-ndk-${NDK_VERSION} - ANDROID_NDK_HOME=$HOME/.android/android-ndk-${NDK_VERSION}
...@@ -13,7 +13,7 @@ env: ...@@ -13,7 +13,7 @@ env:
- PATH=${ANDROID_NDK_HOME}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools:${PATH} - PATH=${ANDROID_NDK_HOME}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools:${PATH}
scala: scala:
- 2.11.8 - 2.11.11
before_cache: before_cache:
- find $HOME/.sbt -name "*.lock" | xargs rm - find $HOME/.sbt -name "*.lock" | xargs rm
...@@ -27,7 +27,7 @@ cache: ...@@ -27,7 +27,7 @@ cache:
android: android:
components: components:
- tools - tools
- build-tools-25.0.2 - build-tools-26.0.0
- extra-android-m2repository - extra-android-m2repository
- extra-google-m2repository - extra-google-m2repository
......
...@@ -14,9 +14,9 @@ A [shadowsocks](http://shadowsocks.org) client for Android, written in Scala. ...@@ -14,9 +14,9 @@ A [shadowsocks](http://shadowsocks.org) client for Android, written in Scala.
* SBT 0.13.0+ * SBT 0.13.0+
* Go 1.4+ * Go 1.4+
* Android SDK * Android SDK
- Build Tools 25+ - Build Tools 26+
- Android Support Repository and Google Repository (see `build.sbt` for version) - Android Support Repository and Google Repository (see `build.sbt` for version)
- Android NDK r14+ - Android NDK r15+
### BUILD ### BUILD
......
...@@ -4,7 +4,7 @@ lazy val commonSettings = Seq( ...@@ -4,7 +4,7 @@ lazy val commonSettings = Seq(
organization := "com.github.shadowsocks", organization := "com.github.shadowsocks",
platformTarget := "android-25", platformTarget := "android-26",
compileOrder := CompileOrder.JavaThenScala, compileOrder := CompileOrder.JavaThenScala,
javacOptions ++= "-source" :: "1.7" :: "-target" :: "1.7" :: Nil, javacOptions ++= "-source" :: "1.7" :: "-target" :: "1.7" :: Nil,
...@@ -18,10 +18,12 @@ lazy val commonSettings = Seq( ...@@ -18,10 +18,12 @@ lazy val commonSettings = Seq(
shrinkResources := true, shrinkResources := true,
typedResources := false, typedResources := false,
resConfigs := Seq("ja", "ko", "ru", "zh-rCN", "zh-rTW") resConfigs := Seq("ja", "ko", "ru", "zh-rCN", "zh-rTW"),
resolvers += "google" at "https://maven.google.com"
) )
val supportLibsVersion = "25.3.1" val supportLibsVersion = "26.0.0-beta2"
lazy val root = Project(id = "shadowsocks-android", base = file(".")) lazy val root = Project(id = "shadowsocks-android", base = file("."))
.settings(commonSettings) .settings(commonSettings)
.aggregate(plugin, mobile) .aggregate(plugin, mobile)
......
...@@ -4,8 +4,8 @@ enablePlugins(AndroidGms) ...@@ -4,8 +4,8 @@ enablePlugins(AndroidGms)
android.useSupportVectors android.useSupportVectors
name := "shadowsocks" name := "shadowsocks"
version := "4.1.7" version := "4.2.0"
versionCode := Some(187) versionCode := Some(190)
proguardOptions ++= proguardOptions ++=
"-dontwarn com.google.android.gms.internal.**" :: "-dontwarn com.google.android.gms.internal.**" ::
...@@ -16,11 +16,11 @@ proguardOptions ++= ...@@ -16,11 +16,11 @@ proguardOptions ++=
"-keep class com.github.shadowsocks.JniHelper { *; }" :: "-keep class com.github.shadowsocks.JniHelper { *; }" ::
Nil Nil
val playServicesVersion = "10.2.6" val playServicesVersion = "11.0.2"
resolvers += Resolver.jcenterRepo resolvers += Resolver.jcenterRepo
libraryDependencies ++= libraryDependencies ++=
"com.futuremind.recyclerfastscroll" % "fastscroll" % "0.2.5" :: "com.futuremind.recyclerfastscroll" % "fastscroll" % "0.2.5" ::
"com.evernote" % "android-job" % "1.1.10" :: "com.evernote" % "android-job" % "1.2.0-alpha3" ::
"com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01" :: "com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01" ::
"com.google.android.gms" % "play-services-ads" % playServicesVersion :: "com.google.android.gms" % "play-services-ads" % playServicesVersion ::
"com.google.android.gms" % "play-services-analytics" % playServicesVersion :: "com.google.android.gms" % "play-services-analytics" % playServicesVersion ::
...@@ -28,14 +28,15 @@ libraryDependencies ++= ...@@ -28,14 +28,15 @@ libraryDependencies ++=
"com.google.firebase" % "firebase-config" % playServicesVersion :: "com.google.firebase" % "firebase-config" % playServicesVersion ::
"com.j256.ormlite" % "ormlite-android" % "5.0" :: "com.j256.ormlite" % "ormlite-android" % "5.0" ::
"com.mikepenz" % "crossfader" % "1.5.0" :: "com.mikepenz" % "crossfader" % "1.5.0" ::
"com.mikepenz" % "fastadapter" % "2.5.2" :: "com.mikepenz" % "fastadapter" % "2.6.2" ::
"com.mikepenz" % "iconics-core" % "2.8.4" :: "com.mikepenz" % "iconics-core" % "2.8.9" ::
"com.mikepenz" % "materialdrawer" % "5.9.1" :: "com.mikepenz" % "materialdrawer" % "5.9.4" ::
"com.mikepenz" % "materialize" % "1.0.1" :: "com.mikepenz" % "materialize" % "1.0.2" ::
"com.squareup.okhttp3" % "okhttp" % "3.8.0" :: "com.squareup.okhttp3" % "okhttp" % "3.8.1" ::
"com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" :: "com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" ::
"dnsjava" % "dnsjava" % "2.1.8" :: "dnsjava" % "dnsjava" % "2.1.8" ::
"eu.chainfire" % "libsuperuser" % "1.0.0.201704021214" :: "eu.chainfire" % "libsuperuser" % "1.0.0.201704021214" ::
"me.dm7.barcodescanner" % "zxing" % "1.9.3" ::
"net.glxn.qrgen" % "android" % "2.0" :: "net.glxn.qrgen" % "android" % "2.0" ::
Nil Nil
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.NFC" /> <uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.touchscreen" <uses-feature android:name="android.hardware.touchscreen"
android:required="false"/> android:required="false"/>
...@@ -11,16 +12,22 @@ ...@@ -11,16 +12,22 @@
android:required="false"/> android:required="false"/>
<uses-feature android:name="android.software.leanback" <uses-feature android:name="android.software.leanback"
android:required="false"/> android:required="false"/>
<uses-feature android:name="android.hardware.camera"
android:required="false"/>
<uses-sdk <uses-sdk
android:minSdkVersion="19" android:minSdkVersion="19"
android:targetSdkVersion="25"/> android:targetSdkVersion="26"/>
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
android:value="true" />
<application <application
android:name=".ShadowsocksApplication" android:name=".ShadowsocksApplication"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:backupAgent=".ShadowsocksBackupAgent" android:backupAgent=".ShadowsocksBackupAgent"
android:label="@string/app_name" android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:banner="@drawable/ic_start_connected"> android:banner="@drawable/ic_start_connected">
<meta-data android:name="com.google.android.gms.version" <meta-data android:name="com.google.android.gms.version"
...@@ -75,6 +82,12 @@ ...@@ -75,6 +82,12 @@
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:launchMode="singleTask"/> android:launchMode="singleTask"/>
<activity
android:name=".ScannerActivity"
android:label="@string/add_profile_methods_scan_qr_code"
android:parentActivityName=".MainActivity"
android:excludeFromRecents="true"/>
<activity android:name=".TaskerActivity" <activity android:name=".TaskerActivity"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<intent-filter> <intent-filter>
...@@ -87,18 +100,14 @@ ...@@ -87,18 +100,14 @@
android:label="@string/quick_toggle" android:label="@string/quick_toggle"
android:theme="@android:style/Theme.Translucent.NoTitleBar" android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:taskAffinity="" android:taskAffinity=""
android:process=":bg"
android:launchMode="singleTask"> android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" /> <action android:name="android.intent.action.CREATE_SHORTCUT" />
</intent-filter> </intent-filter>
</activity> </activity>
<service
android:name=".ShadowsocksRunnerService"
android:exported="false">
</service>
<service <service
android:name=".ShadowsocksNatService" android:name=".ShadowsocksNatService"
android:process=":bg" android:process=":bg"
...@@ -125,13 +134,13 @@ ...@@ -125,13 +134,13 @@
</intent-filter> </intent-filter>
</service> </service>
<receiver android:name=".BootReceiver" android:enabled="false"> <receiver android:name=".BootReceiver" android:process=":bg" android:enabled="false">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".TaskerReceiver" tools:ignore="ExportedReceiver"> <receiver android:name=".TaskerReceiver" android:process=":bg" tools:ignore="ExportedReceiver">
<intent-filter> <intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING"/> <action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING"/>
</intent-filter> </intent-filter>
...@@ -150,9 +159,6 @@ ...@@ -150,9 +159,6 @@
android:process=":bg"/> android:process=":bg"/>
<receiver android:name="com.evernote.android.job.JobBootReceiver" <receiver android:name="com.evernote.android.job.JobBootReceiver"
android:process=":bg"/> android:process=":bg"/>
<!-- Strip useless components -->
<activity android:name="com.google.android.gms.ads.purchase.InAppPurchaseActivity" tools:node="remove"/>
</application> </application>
</manifest> </manifest>
...@@ -10,7 +10,4 @@ interface IShadowsocksService { ...@@ -10,7 +10,4 @@ interface IShadowsocksService {
oneway void startListeningForBandwidth(IShadowsocksServiceCallback cb); oneway void startListeningForBandwidth(IShadowsocksServiceCallback cb);
oneway void stopListeningForBandwidth(IShadowsocksServiceCallback cb); oneway void stopListeningForBandwidth(IShadowsocksServiceCallback cb);
oneway void unregisterCallback(IShadowsocksServiceCallback cb); oneway void unregisterCallback(IShadowsocksServiceCallback cb);
oneway void use(in int profileId);
void useSync(in int profileId);
} }
mobile/src/main/ic_launcher-web.png

34.3 KB | W: | H:

mobile/src/main/ic_launcher-web.png

41.4 KB | W: | H:

mobile/src/main/ic_launcher-web.png
mobile/src/main/ic_launcher-web.png
mobile/src/main/ic_launcher-web.png
mobile/src/main/ic_launcher-web.png
  • 2-up
  • Swipe
  • Onion skin
<?xml version="1.0" encoding="utf-8"?>
<gradient xmlns:android="http://schemas.android.com/apk/res/android"
android:startColor="#19000000"
android:endColor="#00000000"
android:startX="46.15"
android:endX="108"
android:startY="40.34"
android:endY="102.18"
android:type="linear"/>
<vector android:height="108dp" android:viewportHeight="108.0"
android:viewportWidth="108.0" android:width="108dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@color/ic_launcher_foreground_shadow"
android:pathData="M70.94,35.7l37.06,36.3l0,36l-23.92,0l-53.02,-52.57l39.88,-19.73l0,0"/>
<path android:fillAlpha="0.79" android:fillColor="#fff"
android:pathData="M70.94,35.7l-6.94,30.53l-15.56,-5l13.74,-16.38l-18.31,15.4l-12.81,-4.82l39.88,-19.73z"/>
<path android:fillAlpha="0.79" android:fillColor="#fff"
android:pathData="M48.79,64.37l4.96,1.6l-4.94,6.73l-0.02,-8.33z"/>
<path android:fillAlpha="0.79" android:fillColor="#fff"
android:pathData="M70.94,35.7l-6.94,30.53l-15.56,-5l13.74,-16.38l-18.31,15.4l-12.81,-4.82l39.88,-19.73z"/>
<path android:fillAlpha="0.79" android:fillColor="#fff"
android:pathData="M48.79,64.37l4.96,1.6l-4.94,6.73l-0.02,-8.33z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar_light_dark" />
<me.dm7.barcodescanner.zxing.ZXingScannerView android:id="@+id/scanner"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
...@@ -91,6 +91,7 @@ ...@@ -91,6 +91,7 @@
<string name="add_profile_methods_scan_qr_code">"QR コードを読み取る"</string> <string name="add_profile_methods_scan_qr_code">"QR コードを読み取る"</string>
<string name="add_profile_methods_manual_settings">"手動設定"</string> <string name="add_profile_methods_manual_settings">"手動設定"</string>
<string name="add_profile_scanner_not_installed">"Zxingに準拠したQRコードスキャンアプリをインストールして下さい"</string> <string name="add_profile_scanner_not_installed">"Zxingに準拠したQRコードスキャンアプリをインストールして下さい"</string>
<string name="add_profile_scanner_permission_required">QRコードを読み取るにはカメラの使用権限が必要です。</string>
<plurals name="removed"> <plurals name="removed">
<item quantity="other">"削除済み"</item> <item quantity="other">"削除済み"</item>
</plurals> </plurals>
...@@ -132,4 +133,4 @@ ...@@ -132,4 +133,4 @@
<string name="plugin_unknown">"不明なプラグイン"</string> <string name="plugin_unknown">"不明なプラグイン"</string>
<string name="plugin_untrusted">"警告:このプラグインは信頼されていないソースからの可能性があります"</string> <string name="plugin_untrusted">"警告:このプラグインは信頼されていないソースからの可能性があります"</string>
<string name="profile_plugin">"プラグイン"</string> <string name="profile_plugin">"プラグイン"</string>
</resources> </resources>
\ No newline at end of file
...@@ -96,6 +96,7 @@ ...@@ -96,6 +96,7 @@
<string name="add_profile_methods_scan_qr_code">"Сканировать QR-код"</string> <string name="add_profile_methods_scan_qr_code">"Сканировать QR-код"</string>
<string name="add_profile_methods_manual_settings">"Ручные настройки"</string> <string name="add_profile_methods_manual_settings">"Ручные настройки"</string>
<string name="add_profile_scanner_not_installed">"Пожалуйста, установите любое ZXing-совместимое приложение для сканирования QR-кодов."</string> <string name="add_profile_scanner_not_installed">"Пожалуйста, установите любое ZXing-совместимое приложение для сканирования QR-кодов."</string>
<string name="add_profile_scanner_permission_required">Разрешение камеры требуется для сканирования QR код.</string>
<plurals name="removed"> <plurals name="removed">
<item quantity="one">"Удалено"</item> <item quantity="one">"Удалено"</item>
<item quantity="few">"Удалено %d элемента"</item> <item quantity="few">"Удалено %d элемента"</item>
...@@ -140,4 +141,4 @@ ...@@ -140,4 +141,4 @@
<string name="plugin_unknown">"Неизвестный плагин %s"</string> <string name="plugin_unknown">"Неизвестный плагин %s"</string>
<string name="plugin_untrusted">"Предупреждение: этот плагин получен из недоверенного источника."</string> <string name="plugin_untrusted">"Предупреждение: этот плагин получен из недоверенного источника."</string>
<string name="profile_plugin">"Плагин: %s"</string> <string name="profile_plugin">"Плагин: %s"</string>
</resources> </resources>
\ No newline at end of file
...@@ -92,6 +92,7 @@ ...@@ -92,6 +92,7 @@
<string name="add_profile_methods_scan_qr_code">"扫描二维码"</string> <string name="add_profile_methods_scan_qr_code">"扫描二维码"</string>
<string name="add_profile_methods_manual_settings">"手动设置"</string> <string name="add_profile_methods_manual_settings">"手动设置"</string>
<string name="add_profile_scanner_not_installed">"请安装任意兼容 ZXing 的二维码扫描应用。"</string> <string name="add_profile_scanner_not_installed">"请安装任意兼容 ZXing 的二维码扫描应用。"</string>
<string name="add_profile_scanner_permission_required">扫描二维码需要相机权限。</string>
<plurals name="removed"> <plurals name="removed">
<item quantity="other">"已删除 %d 项"</item> <item quantity="other">"已删除 %d 项"</item>
</plurals> </plurals>
...@@ -133,4 +134,4 @@ ...@@ -133,4 +134,4 @@
<string name="plugin_unknown">"未知插件 %s"</string> <string name="plugin_unknown">"未知插件 %s"</string>
<string name="plugin_untrusted">"警告:该插件似乎并非来自已知的可信源。"</string> <string name="plugin_untrusted">"警告:该插件似乎并非来自已知的可信源。"</string>
<string name="profile_plugin">"插件:%s"</string> <string name="profile_plugin">"插件:%s"</string>
</resources> </resources>
\ No newline at end of file
...@@ -90,6 +90,7 @@ ...@@ -90,6 +90,7 @@
<string name="add_profile_methods_scan_qr_code">"掃描 QR 碼"</string> <string name="add_profile_methods_scan_qr_code">"掃描 QR 碼"</string>
<string name="add_profile_methods_manual_settings">"手動設定"</string> <string name="add_profile_methods_manual_settings">"手動設定"</string>
<string name="add_profile_scanner_not_installed">"請安裝任何相容 ZXing 的 QR 碼掃描應用程式。"</string> <string name="add_profile_scanner_not_installed">"請安裝任何相容 ZXing 的 QR 碼掃描應用程式。"</string>
<string name="add_profile_scanner_permission_required">掃描 QR 碼需要相機權限。</string>
<plurals name="removed"> <plurals name="removed">
<item quantity="other">"已移除 %d 項"</item> <item quantity="other">"已移除 %d 項"</item>
</plurals> </plurals>
......
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#7488A1</color>
</resources>
...@@ -56,6 +56,8 @@ ...@@ -56,6 +56,8 @@
<string name="udp_dns_summary">Forward all DNS requests to remote</string> <string name="udp_dns_summary">Forward all DNS requests to remote</string>
<!-- notification category --> <!-- notification category -->
<string name="service_vpn">VPN Service</string>
<string name="service_nat">NAT Service</string>
<string name="forward_success">Shadowsocks started.</string> <string name="forward_success">Shadowsocks started.</string>
<string name="invalid_server">Invalid server name</string> <string name="invalid_server">Invalid server name</string>
<string name="service_failed">Failed to connect the remote server</string> <string name="service_failed">Failed to connect the remote server</string>
...@@ -104,6 +106,7 @@ ...@@ -104,6 +106,7 @@
<string name="add_profile_methods_scan_qr_code">Scan QR code</string> <string name="add_profile_methods_scan_qr_code">Scan QR code</string>
<string name="add_profile_methods_manual_settings">Manual Settings</string> <string name="add_profile_methods_manual_settings">Manual Settings</string>
<string name="add_profile_scanner_not_installed">Please install any ZXing-compliant QR code scanning app.</string> <string name="add_profile_scanner_not_installed">Please install any ZXing-compliant QR code scanning app.</string>
<string name="add_profile_scanner_permission_required">Camera permission is required for scanning QR code.</string>
<plurals name="removed"> <plurals name="removed">
<item quantity="one">Removed</item> <item quantity="one">Removed</item>
<item quantity="other">%d items removed</item> <item quantity="other">%d items removed</item>
......
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false"/>
</network-security-config>
...@@ -34,7 +34,7 @@ class AboutFragment extends ToolbarFragment { ...@@ -34,7 +34,7 @@ class AboutFragment extends ToolbarFragment {
override def onViewCreated(view: View, savedInstanceState: Bundle) { override def onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
toolbar.setTitle(getString(R.string.about_title).formatLocal(Locale.ENGLISH, BuildConfig.VERSION_NAME)) toolbar.setTitle(getString(R.string.about_title).formatLocal(Locale.ENGLISH, BuildConfig.VERSION_NAME))
val web = view.findViewById(R.id.web_view).asInstanceOf[WebView] val web = view.findViewById[WebView](R.id.web_view)
web.loadUrl("file:///android_asset/pages/about.html") web.loadUrl("file:///android_asset/pages/about.html")
web.setWebViewClient(new WebViewClient() { web.setWebViewClient(new WebViewClient() {
override def shouldOverrideUrlLoading(view: WebView, url: String): Boolean = { override def shouldOverrideUrlLoading(view: WebView, url: String): Boolean = {
......
...@@ -70,8 +70,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { ...@@ -70,8 +70,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
import AppManager._ import AppManager._
private final class AppViewHolder(val view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener { private final class AppViewHolder(val view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener {
private val icon = itemView.findViewById(R.id.itemicon).asInstanceOf[ImageView] private val icon = itemView.findViewById[ImageView](R.id.itemicon)
private val check = itemView.findViewById(R.id.itemcheck).asInstanceOf[Switch] private val check = itemView.findViewById[Switch](R.id.itemcheck)
private var item: ProxiedApp = _ private var item: ProxiedApp = _
itemView.setOnClickListener(this) itemView.setOnClickListener(this)
...@@ -92,7 +92,10 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { ...@@ -92,7 +92,10 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
proxiedApps.add(item.packageName) proxiedApps.add(item.packageName)
check.setChecked(true) check.setChecked(true)
} }
if (!appsLoading.get) app.editor.putString(Key.individual, proxiedApps.mkString("\n")).apply() if (!appsLoading.get) {
app.dataStore.individual = proxiedApps.mkString("\n")
app.dataStore.dirty = true
}
} }
} }
...@@ -118,7 +121,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { ...@@ -118,7 +121,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
private val appsLoading = new AtomicBoolean private val appsLoading = new AtomicBoolean
private var handler: Handler = _ private var handler: Handler = _
private def initProxiedApps(str: String = app.settings.getString(Key.individual, null)) = private def initProxiedApps(str: String = app.dataStore.individual) =
proxiedApps = str.split('\n').to[mutable.HashSet] proxiedApps = str.split('\n').to[mutable.HashSet]
override def onDestroy() { override def onDestroy() {
...@@ -136,7 +139,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { ...@@ -136,7 +139,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
case R.id.action_apply_all => case R.id.action_apply_all =>
app.profileManager.getAllProfiles match { app.profileManager.getAllProfiles match {
case Some(profiles) => case Some(profiles) =>
val proxiedAppString = app.settings.getString(Key.individual, null) val proxiedAppString = app.dataStore.individual
profiles.foreach(p => { profiles.foreach(p => {
p.individual = proxiedAppString p.individual = proxiedAppString
app.profileManager.updateProfile(p) app.profileManager.updateProfile(p)
...@@ -146,10 +149,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { ...@@ -146,10 +149,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
} }
return true return true
case R.id.action_export => case R.id.action_export =>
val bypass = app.settings.getBoolean(Key.bypass, false) clipboard.setPrimaryClip(ClipData.newPlainText(Key.individual,
val proxiedAppString = app.settings.getString(Key.individual, null) app.dataStore.bypass + "\n" + app.dataStore.individual))
val clip = ClipData.newPlainText(Key.individual, bypass + "\n" + proxiedAppString)
clipboard.setPrimaryClip(clip)
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
return true return true
case R.id.action_import => case R.id.action_import =>
...@@ -163,7 +164,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { ...@@ -163,7 +164,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
val (enabled, apps) = if (i < 0) (proxiedAppString, "") val (enabled, apps) = if (i < 0) (proxiedAppString, "")
else (proxiedAppString.substring(0, i), proxiedAppString.substring(i + 1)) else (proxiedAppString.substring(0, i), proxiedAppString.substring(i + 1))
bypassSwitch.setChecked(enabled.toBoolean) bypassSwitch.setChecked(enabled.toBoolean)
app.editor.putString(Key.individual, apps).putBoolean(Key.dirty, true).apply() app.dataStore.individual = apps
app.dataStore.dirty = true
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
appListView.setVisibility(View.GONE) appListView.setVisibility(View.GONE)
fastScroller.setVisibility(View.GONE) fastScroller.setVisibility(View.GONE)
...@@ -202,18 +204,23 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { ...@@ -202,18 +204,23 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
toolbar.inflateMenu(R.menu.app_manager_menu) toolbar.inflateMenu(R.menu.app_manager_menu)
toolbar.setOnMenuItemClickListener(this) toolbar.setOnMenuItemClickListener(this)
if (!app.settings.getBoolean(Key.proxyApps, false)) if (!app.dataStore.proxyApps) {
app.editor.putBoolean(Key.proxyApps, true).putBoolean(Key.dirty, true).apply() app.dataStore.proxyApps = true
app.dataStore.dirty = true
}
findViewById(R.id.onSwitch).asInstanceOf[Switch] findViewById(R.id.onSwitch).asInstanceOf[Switch]
.setOnCheckedChangeListener((_, checked) => { .setOnCheckedChangeListener((_, checked) => {
app.editor.putBoolean(Key.proxyApps, checked).putBoolean(Key.dirty, true).apply() app.dataStore.proxyApps = checked
app.dataStore.dirty = true
finish() finish()
}) })
bypassSwitch = findViewById(R.id.bypassSwitch).asInstanceOf[Switch] bypassSwitch = findViewById(R.id.bypassSwitch).asInstanceOf[Switch]
bypassSwitch.setChecked(app.settings.getBoolean(Key.bypass, false)) bypassSwitch.setChecked(app.dataStore.bypass)
bypassSwitch.setOnCheckedChangeListener((_, checked) => bypassSwitch.setOnCheckedChangeListener((_, checked) => {
app.editor.putBoolean(Key.bypass, checked).putBoolean(Key.dirty, true).apply()) app.dataStore.bypass = checked
app.dataStore.dirty = true
})
initProxiedApps() initProxiedApps()
loadingView = findViewById(R.id.loading) loadingView = findViewById(R.id.loading)
......
...@@ -21,15 +21,14 @@ ...@@ -21,15 +21,14 @@
package com.github.shadowsocks package com.github.shadowsocks
import java.io.{File, IOException} import java.io.{File, IOException}
import java.net.{Inet6Address, InetAddress}
import java.util import java.util
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.{Timer, TimerTask} import java.util.{Timer, TimerTask}
import java.net.InetAddress
import java.net.Inet6Address
import android.app.Service import android.app.Service
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter} import android.content._
import android.os.{Handler, IBinder, RemoteCallbackList} import android.os.{Build, Handler, IBinder, RemoteCallbackList}
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
...@@ -65,9 +64,12 @@ trait BaseService extends Service { ...@@ -65,9 +64,12 @@ trait BaseService extends Service {
lazy val handler = new Handler(getMainLooper) lazy val handler = new Handler(getMainLooper)
lazy val restartHanlder = new Handler(getMainLooper) lazy val restartHanlder = new Handler(getMainLooper)
private val closeReceiver: BroadcastReceiver = (context: Context, _: Intent) => { private var notification: ShadowsocksNotification = _
Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show() private val closeReceiver: BroadcastReceiver = (context: Context, intent: Intent) => intent.getAction match {
stopRunner(stopService = true) case Action.RELOAD => forceLoad()
case _ =>
Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show()
stopRunner(stopService = true)
} }
var closeReceiverRegistered: Boolean = _ var closeReceiverRegistered: Boolean = _
...@@ -103,20 +105,6 @@ trait BaseService extends Service { ...@@ -103,20 +105,6 @@ trait BaseService extends Service {
stopListeningForBandwidth(cb) // saves an RPC, and safer stopListeningForBandwidth(cb) // saves an RPC, and safer
callbacks.unregister(cb) callbacks.unregister(cb)
} }
override def use(profileId: Int): Unit = synchronized(if (profileId < 0) stopRunner(stopService = true) else {
val profile = app.profileManager.getProfile(profileId).orNull
if (profile == null) stopRunner(stopService = true, getString(R.string.profile_empty)) else state match {
case State.STOPPED => if (checkProfile(profile)) startRunner(profile)
case State.CONNECTED => if (profileId != BaseService.this.profile.id && checkProfile(profile)) {
stopRunner(stopService = false)
startRunner(profile)
}
case _ => Log.w(BaseService.this.getClass.getSimpleName, "Illegal state when invoking use: " + state)
}
})
override def useSync(profileId: Int): Unit = use(profileId)
} }
def checkProfile(profile: Profile): Boolean = if (TextUtils.isEmpty(profile.host) || TextUtils.isEmpty(profile.password)) { def checkProfile(profile: Profile): Boolean = if (TextUtils.isEmpty(profile.host) || TextUtils.isEmpty(profile.password)) {
...@@ -124,6 +112,17 @@ trait BaseService extends Service { ...@@ -124,6 +112,17 @@ trait BaseService extends Service {
false false
} else true } else true
def forceLoad(): Unit = app.currentProfile.orNull match {
case null => stopRunner(stopService = true, getString(R.string.profile_empty))
case p => if (checkProfile(p)) state match {
case State.STOPPED => startRunner()
case State.CONNECTED =>
stopRunner(stopService = false)
startRunner()
case s => Log.w(BaseService.this.getClass.getSimpleName, "Illegal state when invoking use: " + s)
}
}
def connect() { def connect() {
profile.name = profile.getName // save original name before it's (possibly) overwritten by IP addresses profile.name = profile.getName // save original name before it's (possibly) overwritten by IP addresses
...@@ -160,36 +159,9 @@ trait BaseService extends Service { ...@@ -160,36 +159,9 @@ trait BaseService extends Service {
pluginPath = PluginManager.init(plugin) pluginPath = PluginManager.init(plugin)
} }
def startRunner(profile: Profile) { def createNotification(): ShadowsocksNotification
this.profile = profile def startRunner(): Unit = if (Build.VERSION.SDK_INT >= 26) startForegroundService(new Intent(this, getClass))
else startService(new Intent(this, getClass))
startService(new Intent(this, getClass))
TrafficMonitor.reset()
trafficMonitorThread = new TrafficMonitorThread(getApplicationContext)
trafficMonitorThread.start()
if (!closeReceiverRegistered) {
// register close receiver
val filter = new IntentFilter()
filter.addAction(Intent.ACTION_SHUTDOWN)
filter.addAction(Action.CLOSE)
registerReceiver(closeReceiver, filter)
closeReceiverRegistered = true
}
app.track(getClass.getSimpleName, "start")
changeState(State.CONNECTING)
Utils.ThrowableFuture(try connect() catch {
case _: NameNotResolvedException => stopRunner(stopService = true, getString(R.string.invalid_server))
case _: NullConnectionException => stopRunner(stopService = true, getString(R.string.reboot_required))
case exc: Throwable =>
stopRunner(stopService = true, getString(R.string.service_failed) + ": " + exc.getMessage)
exc.printStackTrace()
app.track(exc)
})
}
def stopRunner(stopService: Boolean, msg: String = null) { def stopRunner(stopService: Boolean, msg: String = null) {
// clean up recevier // clean up recevier
...@@ -198,6 +170,8 @@ trait BaseService extends Service { ...@@ -198,6 +170,8 @@ trait BaseService extends Service {
closeReceiverRegistered = false closeReceiverRegistered = false
} }
if (notification != null) notification.destroy()
// Make sure update total traffic when stopping the runner // Make sure update total traffic when stopping the runner
updateTrafficTotal(TrafficMonitor.txTotal, TrafficMonitor.rxTotal) updateTrafficTotal(TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
...@@ -276,7 +250,41 @@ trait BaseService extends Service { ...@@ -276,7 +250,41 @@ trait BaseService extends Service {
} }
// Service of shadowsocks should always be started explicitly // Service of shadowsocks should always be started explicitly
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = Service.START_NOT_STICKY override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = {
state match {
case State.STOPPED | State.IDLE =>
case _ => return Service.START_NOT_STICKY // ignore request
}
profile = app.currentProfile.orNull
TrafficMonitor.reset()
trafficMonitorThread = new TrafficMonitorThread(getApplicationContext)
trafficMonitorThread.start()
if (!closeReceiverRegistered) {
// register close receiver
val filter = new IntentFilter()
filter.addAction(Action.RELOAD)
filter.addAction(Intent.ACTION_SHUTDOWN)
filter.addAction(Action.CLOSE)
registerReceiver(closeReceiver, filter)
closeReceiverRegistered = true
}
notification = createNotification()
app.track(getClass.getSimpleName, "start")
changeState(State.CONNECTING)
Utils.ThrowableFuture(try connect() catch {
case _: NameNotResolvedException => stopRunner(stopService = true, getString(R.string.invalid_server))
case _: NullConnectionException => stopRunner(stopService = true, getString(R.string.reboot_required))
case exc: Throwable =>
stopRunner(stopService = true, getString(R.string.service_failed) + ": " + exc.getMessage)
exc.printStackTrace()
app.track(exc)
})
Service.START_NOT_STICKY
}
protected def changeState(s: Int, msg: String = null) { protected def changeState(s: Int, msg: String = null) {
val handler = new Handler(getMainLooper) val handler = new Handler(getMainLooper)
......
...@@ -20,27 +20,24 @@ ...@@ -20,27 +20,24 @@
package com.github.shadowsocks package com.github.shadowsocks
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Bundle import android.os.Bundle
import android.support.design.widget.Snackbar import android.support.design.widget.Snackbar
import android.support.v14.preference.SwitchPreference import android.support.v14.preference.SwitchPreference
import android.support.v7.preference.PreferenceDataStore
import be.mygod.preference.PreferenceFragment import be.mygod.preference.PreferenceFragment
import com.github.shadowsocks.utils.{Key, TcpFastOpen}
import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.utils.{Key, TcpFastOpen}
class GlobalConfigFragment extends PreferenceFragment with OnSharedPreferenceChangeListener { class GlobalConfigFragment extends PreferenceFragment with OnPreferenceDataStoreChangeListener {
override def onCreatePreferences(bundle: Bundle, key: String) { override def onCreatePreferences(bundle: Bundle, key: String) {
getPreferenceManager.setPreferenceDataStore(app.dataStore)
addPreferencesFromResource(R.xml.pref_global) addPreferencesFromResource(R.xml.pref_global)
val switch = findPreference(Key.isAutoConnect).asInstanceOf[SwitchPreference] val switch = findPreference(Key.isAutoConnect).asInstanceOf[SwitchPreference]
switch.setOnPreferenceChangeListener((_, value) => { switch.setOnPreferenceChangeListener((_, value) => {
BootReceiver.setEnabled(getActivity, value.asInstanceOf[Boolean]) BootReceiver.setEnabled(getActivity, value.asInstanceOf[Boolean])
true true
}) })
if (getPreferenceManager.getSharedPreferences.getBoolean(Key.isAutoConnect, false)) {
BootReceiver.setEnabled(getActivity, true)
getPreferenceManager.getSharedPreferences.edit.remove(Key.isAutoConnect).apply()
}
switch.setChecked(BootReceiver.getEnabled(getActivity)) switch.setChecked(BootReceiver.getEnabled(getActivity))
val tfo = findPreference(Key.tfo).asInstanceOf[SwitchPreference] val tfo = findPreference(Key.tfo).asInstanceOf[SwitchPreference]
...@@ -56,16 +53,16 @@ class GlobalConfigFragment extends PreferenceFragment with OnSharedPreferenceCha ...@@ -56,16 +53,16 @@ class GlobalConfigFragment extends PreferenceFragment with OnSharedPreferenceCha
tfo.setEnabled(false) tfo.setEnabled(false)
tfo.setSummary(getString(R.string.tcp_fastopen_summary_unsupported, java.lang.System.getProperty("os.version"))) tfo.setSummary(getString(R.string.tcp_fastopen_summary_unsupported, java.lang.System.getProperty("os.version")))
} }
app.settings.registerOnSharedPreferenceChangeListener(this) app.dataStore.registerChangeListener(this)
} }
override def onDestroy() { override def onDestroy() {
app.settings.unregisterOnSharedPreferenceChangeListener(this) app.dataStore.unregisterChangeListener(this)
super.onDestroy() super.onDestroy()
} }
def onSharedPreferenceChanged(pref: SharedPreferences, key: String): Unit = key match { def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String): Unit = key match {
case Key.isNAT => findPreference(key).asInstanceOf[SwitchPreference].setChecked(pref.getBoolean(key, false)) case Key.isNAT => findPreference(key).asInstanceOf[SwitchPreference].setChecked(store.getBoolean(key, false))
case _ => case _ =>
} }
} }
...@@ -26,7 +26,6 @@ import java.util.Locale ...@@ -26,7 +26,6 @@ import java.util.Locale
import android.app.backup.BackupManager import android.app.backup.BackupManager
import android.app.{Activity, ProgressDialog} import android.app.{Activity, ProgressDialog}
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.content._ import android.content._
import android.net.{Uri, VpnService} import android.net.{Uri, VpnService}
import android.nfc.{NdefMessage, NfcAdapter} import android.nfc.{NdefMessage, NfcAdapter}
...@@ -36,6 +35,7 @@ import android.support.design.widget.{FloatingActionButton, Snackbar} ...@@ -36,6 +35,7 @@ import android.support.design.widget.{FloatingActionButton, Snackbar}
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import android.support.v7.app.AlertDialog import android.support.v7.app.AlertDialog
import android.support.v7.content.res.AppCompatResources import android.support.v7.content.res.AppCompatResources
import android.support.v7.preference.PreferenceDataStore
import android.support.v7.widget.RecyclerView.ViewHolder import android.support.v7.widget.RecyclerView.ViewHolder
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
...@@ -45,6 +45,7 @@ import com.github.jorgecastilloprz.FABProgressCircle ...@@ -45,6 +45,7 @@ import com.github.jorgecastilloprz.FABProgressCircle
import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.acl.{Acl, CustomRulesFragment} import com.github.shadowsocks.acl.{Acl, CustomRulesFragment}
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.utils.CloseUtils.autoDisconnect import com.github.shadowsocks.utils.CloseUtils.autoDisconnect
import com.github.shadowsocks.utils._ import com.github.shadowsocks.utils._
import com.mikepenz.crossfader.Crossfader import com.mikepenz.crossfader.Crossfader
...@@ -67,7 +68,7 @@ object MainActivity { ...@@ -67,7 +68,7 @@ object MainActivity {
} }
class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawerItemClickListener class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawerItemClickListener
with OnSharedPreferenceChangeListener { with OnPreferenceDataStoreChangeListener {
import MainActivity._ import MainActivity._
// UI // UI
...@@ -165,7 +166,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe ...@@ -165,7 +166,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
override def onServiceDisconnected(): Unit = changeState(State.IDLE) override def onServiceDisconnected(): Unit = changeState(State.IDLE)
private def addDisableNatToSnackbar(snackbar: Snackbar) = snackbar.setAction(R.string.switch_to_vpn, (_ => private def addDisableNatToSnackbar(snackbar: Snackbar) = snackbar.setAction(R.string.switch_to_vpn, (_ =>
if (state == State.STOPPED) app.editor.putBoolean(Key.isNAT, false).apply()): View.OnClickListener) if (state == State.STOPPED) app.dataStore.isNAT = false): View.OnClickListener)
override def binderDied(): Unit = handler.post(() => { override def binderDied(): Unit = handler.post(() => {
detachService() detachService()
...@@ -174,7 +175,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe ...@@ -174,7 +175,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
}) })
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = resultCode match { override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = resultCode match {
case Activity.RESULT_OK => bgService.use(app.profileId) case Activity.RESULT_OK => Utils.startSsService(this)
case _ => Log.e(TAG, "Failed to start VpnService") case _ => Log.e(TAG, "Failed to start VpnService")
} }
...@@ -246,7 +247,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe ...@@ -246,7 +247,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
} else drawer = drawerBuilder.build() } else drawer = drawerBuilder.build()
val header = drawer.getHeader val header = drawer.getHeader
val title = header.findViewById(R.id.drawer_title).asInstanceOf[TextView] val title = header.findViewById[TextView](R.id.drawer_title)
val tf = Typefaces.get(this, "fonts/Iceland.ttf") val tf = Typefaces.get(this, "fonts/Iceland.ttf")
if (tf != null) title.setTypeface(tf) if (tf != null) title.setTypeface(tf)
...@@ -256,7 +257,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe ...@@ -256,7 +257,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
txRateText = findViewById(R.id.txRate).asInstanceOf[TextView] txRateText = findViewById(R.id.txRate).asInstanceOf[TextView]
rxText = findViewById(R.id.rx).asInstanceOf[TextView] rxText = findViewById(R.id.rx).asInstanceOf[TextView]
rxRateText = findViewById(R.id.rxRate).asInstanceOf[TextView] rxRateText = findViewById(R.id.rxRate).asInstanceOf[TextView]
findViewById(R.id.stat).setOnClickListener(_ => if (state == State.CONNECTED && app.isVpnEnabled) { findViewById[View](R.id.stat).setOnClickListener(_ => if (state == State.CONNECTED && app.isVpnEnabled) {
testCount += 1 testCount += 1
statusText.setText(R.string.connection_test_testing) statusText.setText(R.string.connection_test_testing)
val id = testCount // it would change by other code val id = testCount // it would change by other code
...@@ -298,8 +299,8 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe ...@@ -298,8 +299,8 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
fab = findViewById(R.id.fab).asInstanceOf[FloatingActionButton] fab = findViewById(R.id.fab).asInstanceOf[FloatingActionButton]
fabProgressCircle = findViewById(R.id.fabProgressCircle).asInstanceOf[FABProgressCircle] fabProgressCircle = findViewById(R.id.fabProgressCircle).asInstanceOf[FABProgressCircle]
fab.setOnClickListener(_ => if (state == State.CONNECTED) bgService.use(-1) else Utils.ThrowableFuture { fab.setOnClickListener(_ => if (state == State.CONNECTED) Utils.stopSsService(this) else Utils.ThrowableFuture {
if (app.isNatEnabled) bgService.use(app.profileId) else { if (app.isNatEnabled) Utils.startSsService(this) else {
val intent = VpnService.prepare(this) val intent = VpnService.prepare(this)
if (intent != null) startActivityForResult(intent, REQUEST_CONNECT) if (intent != null) startActivityForResult(intent, REQUEST_CONNECT)
else handler.post(() => onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null)) else handler.post(() => onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null))
...@@ -313,7 +314,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe ...@@ -313,7 +314,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
changeState(State.IDLE) // reset everything to init state changeState(State.IDLE) // reset everything to init state
handler.post(() => attachService(callback)) handler.post(() => attachService(callback))
app.settings.registerOnSharedPreferenceChangeListener(this) app.dataStore.registerChangeListener(this)
val intent = getIntent val intent = getIntent
if (intent != null) handleShareIntent(intent) if (intent != null) handleShareIntent(intent)
...@@ -350,7 +351,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe ...@@ -350,7 +351,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
.show() .show()
} }
def onSharedPreferenceChanged(pref: SharedPreferences, key: String): Unit = key match { def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String): Unit = key match {
case Key.isNAT => handler.post(() => { case Key.isNAT => handler.post(() => {
detachService() detachService()
attachService(callback) attachService(callback)
...@@ -368,7 +369,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe ...@@ -368,7 +369,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
case DRAWER_PROFILES => displayFragment(new ProfilesFragment) case DRAWER_PROFILES => displayFragment(new ProfilesFragment)
case DRAWER_RECOVERY => case DRAWER_RECOVERY =>
app.track("GlobalConfigFragment", "reset") app.track("GlobalConfigFragment", "reset")
if (bgService != null) bgService.use(-1) Utils.stopSsService(this)
val dialog = ProgressDialog.show(this, "", getString(R.string.recovering), true, false) val dialog = ProgressDialog.show(this, "", getString(R.string.recovering), true, false)
val handler = new Handler { val handler = new Handler {
override def handleMessage(msg: Message): Unit = if (dialog.isShowing && !isDestroyed) dialog.dismiss() override def handleMessage(msg: Message): Unit = if (dialog.isShowing && !isDestroyed) dialog.dismiss()
...@@ -424,7 +425,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe ...@@ -424,7 +425,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
override def onDestroy() { override def onDestroy() {
super.onDestroy() super.onDestroy()
app.settings.unregisterOnSharedPreferenceChangeListener(this) app.dataStore.unregisterChangeListener(this)
detachService() detachService()
new BackupManager(this).dataChanged() new BackupManager(this).dataChanged()
handler.removeCallbacksAndMessages(null) handler.removeCallbacksAndMessages(null)
......
...@@ -49,7 +49,7 @@ class ProfileConfigActivity extends Activity { ...@@ -49,7 +49,7 @@ class ProfileConfigActivity extends Activity {
toolbar.setOnMenuItemClickListener(child) toolbar.setOnMenuItemClickListener(child)
} }
override def onBackPressed(): Unit = if (app.settings.getBoolean(Key.dirty, false)) new AlertDialog.Builder(this) override def onBackPressed(): Unit = if (app.dataStore.dirty) new AlertDialog.Builder(this)
.setTitle(R.string.unsaved_changes_prompt) .setTitle(R.string.unsaved_changes_prompt)
.setPositiveButton(R.string.yes, ((_, _) => child.saveAndExit()): DialogInterface.OnClickListener) .setPositiveButton(R.string.yes, ((_, _) => child.saveAndExit()): DialogInterface.OnClickListener)
.setNegativeButton(R.string.no, ((_, _) => finish()): DialogInterface.OnClickListener) .setNegativeButton(R.string.no, ((_, _) => finish()): DialogInterface.OnClickListener)
......
...@@ -21,13 +21,12 @@ ...@@ -21,13 +21,12 @@
package com.github.shadowsocks package com.github.shadowsocks
import android.app.Activity import android.app.Activity
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.content._ import android.content._
import android.os.{Build, Bundle, UserManager} import android.os.{Build, Bundle, UserManager}
import android.support.design.widget.Snackbar import android.support.design.widget.Snackbar
import android.support.v14.preference.SwitchPreference import android.support.v14.preference.SwitchPreference
import android.support.v7.app.AlertDialog import android.support.v7.app.AlertDialog
import android.support.v7.preference.Preference import android.support.v7.preference.{Preference, PreferenceDataStore}
import android.support.v7.widget.Toolbar.OnMenuItemClickListener import android.support.v7.widget.Toolbar.OnMenuItemClickListener
import android.text.TextUtils import android.text.TextUtils
import android.view.MenuItem import android.view.MenuItem
...@@ -35,7 +34,7 @@ import be.mygod.preference.{EditTextPreference, PreferenceFragment} ...@@ -35,7 +34,7 @@ import be.mygod.preference.{EditTextPreference, PreferenceFragment}
import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.plugin._ import com.github.shadowsocks.plugin._
import com.github.shadowsocks.preference.{IconListPreference, PluginConfigurationDialogFragment} import com.github.shadowsocks.preference.{IconListPreference, OnPreferenceDataStoreChangeListener, PluginConfigurationDialogFragment}
import com.github.shadowsocks.utils.{Action, Key, Utils} import com.github.shadowsocks.utils.{Action, Key, Utils}
object ProfileConfigFragment { object ProfileConfigFragment {
...@@ -43,7 +42,7 @@ object ProfileConfigFragment { ...@@ -43,7 +42,7 @@ object ProfileConfigFragment {
} }
class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListener class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListener
with OnSharedPreferenceChangeListener { with OnPreferenceDataStoreChangeListener {
import ProfileConfigFragment._ import ProfileConfigFragment._
private var profile: Profile = _ private var profile: Profile = _
...@@ -53,10 +52,11 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe ...@@ -53,10 +52,11 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
private var pluginConfiguration: PluginConfiguration = _ private var pluginConfiguration: PluginConfiguration = _
override def onCreatePreferences(bundle: Bundle, key: String) { override def onCreatePreferences(bundle: Bundle, key: String) {
getPreferenceManager.setPreferenceDataStore(app.dataStore)
app.profileManager.getProfile(getActivity.getIntent.getIntExtra(Action.EXTRA_PROFILE_ID, -1)) match { app.profileManager.getProfile(getActivity.getIntent.getIntExtra(Action.EXTRA_PROFILE_ID, -1)) match {
case Some(p) => case Some(p) =>
profile = p profile = p
profile.serialize(app.editor).apply() profile.serialize(app.dataStore)
case None => getActivity.finish() case None => getActivity.finish()
} }
addPreferencesFromResource(R.xml.pref_profile) addPreferencesFromResource(R.xml.pref_profile)
...@@ -78,7 +78,8 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe ...@@ -78,7 +78,8 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
plugin.setOnPreferenceChangeListener((_, value) => { plugin.setOnPreferenceChangeListener((_, value) => {
val selected = value.asInstanceOf[String] val selected = value.asInstanceOf[String]
pluginConfiguration = new PluginConfiguration(pluginConfiguration.pluginsOptions, selected) pluginConfiguration = new PluginConfiguration(pluginConfiguration.pluginsOptions, selected)
app.editor.putString(Key.plugin, pluginConfiguration.toString).putBoolean(Key.dirty, true).apply() app.dataStore.plugin = pluginConfiguration.toString
app.dataStore.dirty = true
pluginConfigure.setEnabled(!TextUtils.isEmpty(selected)) pluginConfigure.setEnabled(!TextUtils.isEmpty(selected))
pluginConfigure.setText(pluginConfiguration.selectedOptions.toString) pluginConfigure.setText(pluginConfiguration.selectedOptions.toString)
if (!PluginManager.fetchPlugins()(selected).trusted) if (!PluginManager.fetchPlugins()(selected).trusted)
...@@ -88,7 +89,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe ...@@ -88,7 +89,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
pluginConfigure.setOnPreferenceChangeListener(onPluginConfigureChanged) pluginConfigure.setOnPreferenceChangeListener(onPluginConfigureChanged)
initPlugins() initPlugins()
app.listenForPackageChanges(initPlugins()) app.listenForPackageChanges(initPlugins())
app.settings.registerOnSharedPreferenceChangeListener(this) app.dataStore.registerChangeListener(this)
} }
def initPlugins() { def initPlugins() {
...@@ -97,7 +98,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe ...@@ -97,7 +98,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
plugin.setEntryValues(plugins.map(_._2.id.asInstanceOf[CharSequence]).toArray) plugin.setEntryValues(plugins.map(_._2.id.asInstanceOf[CharSequence]).toArray)
plugin.setEntryIcons(plugins.map(_._2.icon).toArray) plugin.setEntryIcons(plugins.map(_._2.icon).toArray)
plugin.entryPackageNames = plugins.map(_._2.packageName).toArray plugin.entryPackageNames = plugins.map(_._2.packageName).toArray
pluginConfiguration = new PluginConfiguration(app.settings.getString(Key.plugin, null)) pluginConfiguration = new PluginConfiguration(app.dataStore.plugin)
plugin.setValue(pluginConfiguration.selected) plugin.setValue(pluginConfiguration.selected)
plugin.init() plugin.init()
plugin.checkSummary() plugin.checkSummary()
...@@ -109,7 +110,8 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe ...@@ -109,7 +110,8 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
val selected = pluginConfiguration.selected val selected = pluginConfiguration.selected
pluginConfiguration = new PluginConfiguration(pluginConfiguration.pluginsOptions + pluginConfiguration = new PluginConfiguration(pluginConfiguration.pluginsOptions +
(pluginConfiguration.selected -> new PluginOptions(selected, value.asInstanceOf[String])), selected) (pluginConfiguration.selected -> new PluginOptions(selected, value.asInstanceOf[String])), selected)
app.editor.putString(Key.plugin, pluginConfiguration.toString).putBoolean(Key.dirty, true).apply() app.dataStore.plugin = pluginConfiguration.toString
app.dataStore.dirty = true
true true
} catch { } catch {
case exc: IllegalArgumentException => case exc: IllegalArgumentException =>
...@@ -117,17 +119,17 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe ...@@ -117,17 +119,17 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
false false
} }
override def onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String): Unit = def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String): Unit =
if (key != Key.proxyApps && findPreference(key) != null) app.editor.putBoolean(Key.dirty, true).apply() if (key != Key.proxyApps && findPreference(key) != null) app.dataStore.dirty = true
override def onDestroy() { override def onDestroy() {
app.settings.unregisterOnSharedPreferenceChangeListener(this) app.dataStore.unregisterChangeListener(this)
super.onDestroy() super.onDestroy()
} }
override def onResume() { override def onResume() {
super.onResume() super.onResume()
isProxyApps.setChecked(app.settings.getBoolean(Key.proxyApps, false)) // fetch proxyApps updated by AppManager isProxyApps.setChecked(app.dataStore.proxyApps) // fetch proxyApps updated by AppManager
} }
private def showPluginEditor() { private def showPluginEditor() {
...@@ -176,7 +178,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe ...@@ -176,7 +178,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
} }
def saveAndExit() { def saveAndExit() {
profile.deserialize(app.settings) profile.deserialize(app.dataStore)
app.profileManager.updateProfile(profile) app.profileManager.updateProfile(profile)
if (ProfilesFragment.instance != null) ProfilesFragment.instance.profilesAdapter.deepRefreshId(profile.id) if (ProfilesFragment.instance != null) ProfilesFragment.instance.profilesAdapter.deepRefreshId(profile.id)
getActivity.finish() getActivity.finish()
......
...@@ -68,7 +68,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic ...@@ -68,7 +68,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
case _ => false case _ => false
} }
private def isProfileEditable(id: => Int) = getActivity.asInstanceOf[MainActivity].state match { private def isProfileEditable(id: => Int) = getActivity.asInstanceOf[MainActivity].state match {
case State.CONNECTED => id != app.profileId case State.CONNECTED => id != app.dataStore.profileId
case State.STOPPED => true case State.STOPPED => true
case _ => false case _ => false
} }
...@@ -78,20 +78,20 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic ...@@ -78,20 +78,20 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
var item: Profile = _ var item: Profile = _
private val text1 = itemView.findViewById(android.R.id.text1).asInstanceOf[TextView] private val text1 = itemView.findViewById[TextView](android.R.id.text1)
private val text2 = itemView.findViewById(android.R.id.text2).asInstanceOf[TextView] private val text2 = itemView.findViewById[TextView](android.R.id.text2)
private val traffic = itemView.findViewById(R.id.traffic).asInstanceOf[TextView] private val traffic = itemView.findViewById[TextView](R.id.traffic)
private val edit = itemView.findViewById(R.id.edit) private val edit = itemView.findViewById[View](R.id.edit)
edit.setOnClickListener(_ => startConfig(item.id)) edit.setOnClickListener(_ => startConfig(item.id))
edit.setOnLongClickListener(cardButtonLongClickListener) edit.setOnLongClickListener(cardButtonLongClickListener)
itemView.setOnClickListener(this) itemView.setOnClickListener(this)
// it will not take effect unless set in code // it will not take effect unless set in code
itemView.findViewById(R.id.indicator).setBackgroundResource(R.drawable.background_profile) itemView.findViewById[View](R.id.indicator).setBackgroundResource(R.drawable.background_profile)
private var adView: NativeExpressAdView = _ private var adView: NativeExpressAdView = _
{ {
val share = itemView.findViewById(R.id.share) val share = itemView.findViewById[View](R.id.share)
share.setOnClickListener(_ => { share.setOnClickListener(_ => {
val popup = new PopupMenu(getActivity, share) val popup = new PopupMenu(getActivity, share)
popup.getMenuInflater.inflate(R.menu.profile_share_popup, popup.getMenu) popup.getMenuInflater.inflate(R.menu.profile_share_popup, popup.getMenu)
...@@ -129,7 +129,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic ...@@ -129,7 +129,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
TrafficMonitor.formatTraffic(tx), TrafficMonitor.formatTraffic(rx))) TrafficMonitor.formatTraffic(tx), TrafficMonitor.formatTraffic(rx)))
} }
if (item.id == app.profileId) { if (item.id == app.dataStore.profileId) {
itemView.setSelected(true) itemView.setSelected(true)
selectedItem = this selectedItem = this
} else { } else {
...@@ -147,7 +147,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic ...@@ -147,7 +147,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
adView.setLayoutParams(params) adView.setLayoutParams(params)
adView.setAdUnitId("ca-app-pub-9097031975646651/5224027521") adView.setAdUnitId("ca-app-pub-9097031975646651/5224027521")
adView.setAdSize(new AdSize(328, 132)) adView.setAdSize(new AdSize(328, 132))
itemView.findViewById(R.id.content).asInstanceOf[LinearLayout].addView(adView) itemView.findViewById[LinearLayout](R.id.content).addView(adView)
// Demographics // Demographics
val random = new Random() val random = new Random()
...@@ -166,11 +166,11 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic ...@@ -166,11 +166,11 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
def onClick(v: View): Unit = if (isEnabled) { def onClick(v: View): Unit = if (isEnabled) {
val activity = getActivity.asInstanceOf[MainActivity] val activity = getActivity.asInstanceOf[MainActivity]
val old = app.profileId val old = app.dataStore.profileId
app.switchProfile(item.id) app.switchProfile(item.id)
profilesAdapter.refreshId(old) profilesAdapter.refreshId(old)
itemView.setSelected(true) itemView.setSelected(true)
if (activity.state == State.CONNECTED) activity.bgService.use(item.id) // reconnect to new profile if (activity.state == State.CONNECTED) Utils.reloadSsService(activity)
} }
override def onMenuItemClick(menu: MenuItem): Boolean = menu.getItemId match { override def onMenuItemClick(menu: MenuItem): Boolean = menu.getItemId match {
...@@ -251,7 +251,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic ...@@ -251,7 +251,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
if (index >= 0) { if (index >= 0) {
profiles.remove(index) profiles.remove(index)
notifyItemRemoved(index) notifyItemRemoved(index)
if (id == app.profileId) app.profileId(0) // switch to null profile if (id == app.dataStore.profileId) app.dataStore.profileId = 0 // switch to null profile
} }
} }
} }
...@@ -278,12 +278,12 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic ...@@ -278,12 +278,12 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
toolbar.inflateMenu(R.menu.profile_manager_menu) toolbar.inflateMenu(R.menu.profile_manager_menu)
toolbar.setOnMenuItemClickListener(this) toolbar.setOnMenuItemClickListener(this)
if (app.profileManager.getFirstProfile.isEmpty) app.profileId(app.profileManager.createProfile().id) if (app.profileManager.getFirstProfile.isEmpty) app.dataStore.profileId = app.profileManager.createProfile().id
val profilesList = view.findViewById(R.id.list).asInstanceOf[RecyclerView] val profilesList = view.findViewById[RecyclerView](R.id.list)
val layoutManager = new LinearLayoutManager(getActivity, LinearLayoutManager.VERTICAL, false) val layoutManager = new LinearLayoutManager(getActivity, LinearLayoutManager.VERTICAL, false)
profilesList.setLayoutManager(layoutManager) profilesList.setLayoutManager(layoutManager)
layoutManager.scrollToPosition(profilesAdapter.profiles.zipWithIndex.collectFirst { layoutManager.scrollToPosition(profilesAdapter.profiles.zipWithIndex.collectFirst {
case (profile, i) if profile.id == app.profileId => i case (profile, i) if profile.id == app.dataStore.profileId => i
}.getOrElse(-1)) }.getOrElse(-1))
val animator = new DefaultItemAnimator() val animator = new DefaultItemAnimator()
animator.setSupportsChangeAnimations(false) // prevent fading-in/out when rebinding animator.setSupportsChangeAnimations(false) // prevent fading-in/out when rebinding
...@@ -352,21 +352,16 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic ...@@ -352,21 +352,16 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
def onMenuItemClick(item: MenuItem): Boolean = item.getItemId match { def onMenuItemClick(item: MenuItem): Boolean = item.getItemId match {
case R.id.action_scan_qr_code => case R.id.action_scan_qr_code =>
def installScanner() = {
Toast.makeText(getActivity, R.string.add_profile_scanner_not_installed, Toast.LENGTH_LONG).show()
try startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=tw.com.quickmark"))) catch {
case exc: ActivityNotFoundException => exc.printStackTrace()
}
}
try startActivityForResult(new Intent("com.google.zxing.client.android.SCAN") try startActivityForResult(new Intent("com.google.zxing.client.android.SCAN")
.addCategory(Intent.CATEGORY_DEFAULT) .addCategory(Intent.CATEGORY_DEFAULT)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_DOCUMENT), .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_DOCUMENT),
REQUEST_SCAN_QR_CODE) catch { REQUEST_SCAN_QR_CODE) catch {
case _: ActivityNotFoundException => installScanner() case _: ActivityNotFoundException =>
startActivity(new Intent(getActivity, classOf[ScannerActivity]))
case e: SecurityException => case e: SecurityException =>
e.printStackTrace() e.printStackTrace()
app.track(e) app.track(e)
installScanner() startActivity(new Intent(getActivity, classOf[ScannerActivity]))
} }
true true
case R.id.action_import => case R.id.action_import =>
......
...@@ -24,6 +24,8 @@ import android.app.Activity ...@@ -24,6 +24,8 @@ import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.pm.ShortcutManager import android.content.pm.ShortcutManager
import android.os.{Build, Bundle} import android.os.{Build, Bundle}
import android.support.v4.content.pm.{ShortcutInfoCompat, ShortcutManagerCompat}
import android.support.v4.graphics.drawable.IconCompat
import com.github.shadowsocks.utils.{State, Utils} import com.github.shadowsocks.utils.{State, Utils}
/** /**
...@@ -34,11 +36,12 @@ class QuickToggleShortcut extends Activity with ServiceBoundContext { ...@@ -34,11 +36,12 @@ class QuickToggleShortcut extends Activity with ServiceBoundContext {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
getIntent.getAction match { getIntent.getAction match {
case Intent.ACTION_CREATE_SHORTCUT => case Intent.ACTION_CREATE_SHORTCUT =>
setResult(Activity.RESULT_OK, new Intent() setResult(Activity.RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this,
.putExtra(Intent.EXTRA_SHORTCUT_INTENT, new Intent(this, classOf[QuickToggleShortcut])) new ShortcutInfoCompat.Builder(this, "toggle")
.putExtra(Intent.EXTRA_SHORTCUT_NAME, getString(R.string.quick_toggle)) .setIntent(new Intent(this, classOf[QuickToggleShortcut]).setAction(Intent.ACTION_MAIN))
.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, .setIcon(IconCompat.createWithResource(this, R.drawable.ic_qu_shadowsocks_launcher))
Intent.ShortcutIconResource.fromContext(this, R.mipmap.ic_launcher))) .setShortLabel(getString(R.string.quick_toggle))
.build()))
finish() finish()
case _ => case _ =>
attachService() attachService()
......
/*******************************************************************************/ /*******************************************************************************/
/* */ /* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */ /* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */ /* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */ /* */
/* This program is free software: you can redistribute it and/or modify */ /* 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 */ /* it under the terms of the GNU General Public License as published by */
...@@ -20,38 +20,86 @@ ...@@ -20,38 +20,86 @@
package com.github.shadowsocks package com.github.shadowsocks
import android.app.Service import android.app.{Activity, TaskStackBuilder}
import android.content.Intent import android.content.Intent
import android.net.VpnService import android.content.pm.{PackageManager, ShortcutManager}
import android.os.{Handler, IBinder} import android.os.{Build, Bundle}
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.text.TextUtils
import android.widget.Toast
import com.google.zxing.Result
import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.Parser
import me.dm7.barcodescanner.zxing.ZXingScannerView
class ShadowsocksRunnerService extends Service with ServiceBoundContext { object ScannerActivity {
val handler = new Handler() private final val MY_PERMISSIONS_REQUEST_CAMERA = 1
}
override def onBind(intent: Intent): IBinder = { class ScannerActivity extends AppCompatActivity with ZXingScannerView.ResultHandler {
null import ScannerActivity._
}
private var scannerView: ZXingScannerView = _
override def onServiceConnected() { override def onRequestPermissionsResult(requestCode: Int, permissions: Array[String],
handler.postDelayed(() => { grantResults: Array[Int]) {
if (bgService != null) { if (requestCode == MY_PERMISSIONS_REQUEST_CAMERA) {
if (app.isNatEnabled) startBackgroundService() // If request is cancelled, the result arrays are empty.
else if (VpnService.prepare(ShadowsocksRunnerService.this) == null) startBackgroundService() if (grantResults.length > 0
handler.postDelayed(() => stopSelf(), 3000) && grantResults(0) == PackageManager.PERMISSION_GRANTED) {
scannerView.setResultHandler(this)
scannerView.startCamera()
} else {
Toast.makeText(this, R.string.add_profile_scanner_permission_required, Toast.LENGTH_SHORT).show()
finish()
} }
}, 1000) }
} }
def startBackgroundService(): Unit = bgService.useSync(app.profileId) def navigateUp() {
val intent = getParentActivityIntent
if (shouldUpRecreateTask(intent) || isTaskRoot)
TaskStackBuilder.create(this).addNextIntentWithParentStack(intent).startActivities()
else finish()
}
override def onCreate() { override def onCreate(state: Bundle) {
super.onCreate() super.onCreate(state)
attachService() setContentView(R.layout.layout_scanner)
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
toolbar.setTitle(getTitle)
toolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_material)
toolbar.setNavigationOnClickListener(_ => navigateUp())
scannerView = findViewById(R.id.scanner).asInstanceOf[ZXingScannerView]
if (Build.VERSION.SDK_INT >= 25) getSystemService(classOf[ShortcutManager]).reportShortcutUsed("scan")
} }
override def onDestroy() { override def onResume() {
super.onDestroy() super.onResume()
detachService() val permissionCheck = ContextCompat.checkSelfPermission(this,
android.Manifest.permission.CAMERA)
if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
scannerView.setResultHandler(this) // Register ourselves as a handler for scan results.
scannerView.startCamera() // Start camera on resume
} else {
ActivityCompat.requestPermissions(this,
Array(android.Manifest.permission.CAMERA), MY_PERMISSIONS_REQUEST_CAMERA)
}
}
override def onPause() {
super.onPause()
scannerView.stopCamera() // Stop camera on pause
}
override def handleResult(rawResult: Result) = {
val uri = rawResult.getText
if (!TextUtils.isEmpty(uri))
Parser.findAll(uri).foreach(app.profileManager.createProfile)
navigateUp()
} }
} }
...@@ -24,16 +24,16 @@ import java.io.{File, FileOutputStream, IOException} ...@@ -24,16 +24,16 @@ import java.io.{File, FileOutputStream, IOException}
import java.util.Locale import java.util.Locale
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Application import android.app.{Application, NotificationChannel, NotificationManager}
import android.content._ import android.content._
import android.content.res.Configuration import android.content.res.Configuration
import android.os.{Build, LocaleList} import android.os.{Build, LocaleList}
import android.preference.PreferenceManager
import android.support.v7.app.AppCompatDelegate import android.support.v7.app.AppCompatDelegate
import android.util.Log import android.util.Log
import com.evernote.android.job.JobManager import com.evernote.android.job.JobManager
import com.github.shadowsocks.acl.DonaldTrump import com.github.shadowsocks.acl.DonaldTrump
import com.github.shadowsocks.database.{DBHelper, Profile, ProfileManager} import com.github.shadowsocks.database.{DBHelper, Profile, ProfileManager}
import com.github.shadowsocks.preference.OrmLitePreferenceDataStore
import com.github.shadowsocks.utils.CloseUtils._ import com.github.shadowsocks.utils.CloseUtils._
import com.github.shadowsocks.utils._ import com.github.shadowsocks.utils._
import com.google.android.gms.analytics.{GoogleAnalytics, HitBuilders, StandardExceptionParser, Tracker} import com.google.android.gms.analytics.{GoogleAnalytics, HitBuilders, StandardExceptionParser, Tracker}
...@@ -42,6 +42,7 @@ import com.google.firebase.remoteconfig.FirebaseRemoteConfig ...@@ -42,6 +42,7 @@ import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.j256.ormlite.logger.LocalLog import com.j256.ormlite.logger.LocalLog
import eu.chainfire.libsuperuser.Shell import eu.chainfire.libsuperuser.Shell
import scala.collection.JavaConversions._
import scala.collection.mutable.ArrayBuffer import scala.collection.mutable.ArrayBuffer
object ShadowsocksApplication { object ShadowsocksApplication {
...@@ -59,13 +60,13 @@ object ShadowsocksApplication { ...@@ -59,13 +60,13 @@ object ShadowsocksApplication {
class ShadowsocksApplication extends Application { class ShadowsocksApplication extends Application {
import ShadowsocksApplication._ import ShadowsocksApplication._
lazy val remoteConfig = FirebaseRemoteConfig.getInstance() lazy val remoteConfig: FirebaseRemoteConfig = FirebaseRemoteConfig.getInstance()
lazy val tracker: Tracker = GoogleAnalytics.getInstance(this).newTracker(R.xml.tracker) lazy val tracker: Tracker = GoogleAnalytics.getInstance(this).newTracker(R.xml.tracker)
lazy val settings: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) private lazy val dbHelper = new DBHelper(this)
lazy val editor: SharedPreferences.Editor = settings.edit lazy val profileManager = new ProfileManager(dbHelper)
lazy val profileManager = new ProfileManager(new DBHelper(this)) lazy val dataStore = new OrmLitePreferenceDataStore(dbHelper)
def isNatEnabled: Boolean = settings.getBoolean(Key.isNAT, false) def isNatEnabled: Boolean = dataStore.isNAT
def isVpnEnabled: Boolean = !isNatEnabled def isVpnEnabled: Boolean = !isNatEnabled
// send event // send event
...@@ -78,13 +79,11 @@ class ShadowsocksApplication extends Application { ...@@ -78,13 +79,11 @@ class ShadowsocksApplication extends Application {
.setFatal(false) .setFatal(false)
.build()) .build())
def profileId: Int = settings.getInt(Key.id, 0) def currentProfile: Option[Profile] = profileManager.getProfile(dataStore.profileId)
def profileId(i: Int): Unit = editor.putInt(Key.id, i).apply()
def currentProfile: Option[Profile] = profileManager.getProfile(profileId)
def switchProfile(id: Int): Profile = { def switchProfile(id: Int): Profile = {
val result = profileManager.getProfile(id) getOrElse profileManager.createProfile() val result = profileManager.getProfile(id) getOrElse profileManager.createProfile()
profileId(result.id) dataStore.profileId = result.id
result result
} }
...@@ -153,7 +152,12 @@ class ShadowsocksApplication extends Application { ...@@ -153,7 +152,12 @@ class ShadowsocksApplication extends Application {
JobManager.create(this).addJobCreator(DonaldTrump) JobManager.create(this).addJobCreator(DonaldTrump)
TcpFastOpen.enabled(settings.getBoolean(Key.tfo, TcpFastOpen.sendEnabled)) TcpFastOpen.enabled(dataStore.getBoolean(Key.tfo, TcpFastOpen.sendEnabled))
if (Build.VERSION.SDK_INT >= 26) getSystemService(classOf[NotificationManager]).createNotificationChannels(List(
new NotificationChannel("service-vpn", getText(R.string.service_vpn), NotificationManager.IMPORTANCE_MIN),
new NotificationChannel("service-nat", getText(R.string.service_nat), NotificationManager.IMPORTANCE_LOW)
))
} }
def crashRecovery() { def crashRecovery() {
...@@ -186,10 +190,10 @@ class ShadowsocksApplication extends Application { ...@@ -186,10 +190,10 @@ class ShadowsocksApplication extends Application {
autoClose(new FileOutputStream(new File(getFilesDir, file)))(out => autoClose(new FileOutputStream(new File(getFilesDir, file)))(out =>
IOUtils.copy(in, out))) IOUtils.copy(in, out)))
} }
editor.putInt(Key.currentVersionCode, BuildConfig.VERSION_CODE).apply() dataStore.putInt(Key.currentVersionCode, BuildConfig.VERSION_CODE)
} }
def updateAssets(): Unit = if (settings.getInt(Key.currentVersionCode, -1) != BuildConfig.VERSION_CODE) copyAssets() def updateAssets(): Unit = if (dataStore.getInt(Key.currentVersionCode, -1) != BuildConfig.VERSION_CODE) copyAssets()
def listenForPackageChanges(callback: => Unit): BroadcastReceiver = { def listenForPackageChanges(callback: => Unit): BroadcastReceiver = {
val filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED) val filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED)
......
...@@ -20,26 +20,11 @@ ...@@ -20,26 +20,11 @@
package com.github.shadowsocks package com.github.shadowsocks
import android.app.backup.{BackupAgentHelper, FileBackupHelper, SharedPreferencesBackupHelper} import android.app.backup.{BackupAgentHelper, FileBackupHelper}
import com.github.shadowsocks.acl.Acl import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.database.DBHelper import com.github.shadowsocks.database.DBHelper
class ShadowsocksBackupAgent extends BackupAgentHelper { class ShadowsocksBackupAgent extends BackupAgentHelper {
override def onCreate(): Unit = addHelper("com.github.shadowsocks.database.profile",
// The names of the SharedPreferences groups that the application maintains. These new FileBackupHelper(this, "../databases/" + DBHelper.PROFILE, Acl.CUSTOM_RULES + ".acl"))
// are the same strings that are passed to getSharedPreferences(String, int).
val PREFS_DISPLAY = "com.github.shadowsocks_preferences"
// An arbitrary string used within the BackupAgentHelper implementation to
// identify the SharedPreferencesBackupHelper's data.
val MY_PREFS_BACKUP_KEY = "com.github.shadowsocks"
val DATABASE = "com.github.shadowsocks.database.profile"
override def onCreate() {
val helper = new SharedPreferencesBackupHelper(this, PREFS_DISPLAY)
addHelper(MY_PREFS_BACKUP_KEY, helper)
addHelper(DATABASE, new FileBackupHelper(this, "../databases/" + DBHelper.PROFILE,
Acl.CUSTOM_RULES + ".acl"))
}
} }
...@@ -24,11 +24,12 @@ import java.io.File ...@@ -24,11 +24,12 @@ import java.io.File
import java.net.{Inet6Address, InetAddress} import java.net.{Inet6Address, InetAddress}
import java.util.Locale import java.util.Locale
import android.app.Service
import android.content._ import android.content._
import android.os._ import android.os._
import android.util.Log import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.acl.{AclSyncJob, Acl} import com.github.shadowsocks.acl.{Acl, AclSyncJob}
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils._ import com.github.shadowsocks.utils._
import eu.chainfire.libsuperuser.Shell import eu.chainfire.libsuperuser.Shell
...@@ -43,7 +44,6 @@ class ShadowsocksNatService extends BaseService { ...@@ -43,7 +44,6 @@ class ShadowsocksNatService extends BaseService {
val CMD_IPTABLES_DNAT_ADD_SOCKS = val CMD_IPTABLES_DNAT_ADD_SOCKS =
"iptables -t nat -A OUTPUT -p tcp -j DNAT --to-destination 127.0.0.1:8123" "iptables -t nat -A OUTPUT -p tcp -j DNAT --to-destination 127.0.0.1:8123"
private var notification: ShadowsocksNotification = _
val myUid: Int = android.os.Process.myUid() val myUid: Int = android.os.Process.myUid()
var sslocalProcess: GuardedProcess = _ var sslocalProcess: GuardedProcess = _
...@@ -177,15 +177,17 @@ class ShadowsocksNatService extends BaseService { ...@@ -177,15 +177,17 @@ class ShadowsocksNatService extends BaseService {
su.addCommand((init_sb ++ http_sb).toArray) su.addCommand((init_sb ++ http_sb).toArray)
} }
override def startRunner(profile: Profile): Unit = if (su == null) override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = if (su == null) {
su = new Shell.Builder().useSU().setWantSTDERR(true).setWatchdogTimeout(10).open((_, exitCode, _) => su = new Shell.Builder().useSU().setWantSTDERR(true).setWatchdogTimeout(10).open((_, exitCode, _) =>
if (exitCode == 0) super.startRunner(profile) else { if (exitCode == 0) super.onStartCommand(intent, flags, startId) else {
if (su != null) { if (su != null) {
su.close() su.close()
su = null su = null
} }
super.stopRunner(stopService = true, getString(R.string.nat_no_root)) super.stopRunner(stopService = true, getString(R.string.nat_no_root))
}) })
Service.START_NOT_STICKY
} else super.onStartCommand(intent, flags, startId)
override def connect() { override def connect() {
super.connect() super.connect()
...@@ -204,12 +206,11 @@ class ShadowsocksNatService extends BaseService { ...@@ -204,12 +206,11 @@ class ShadowsocksNatService extends BaseService {
AclSyncJob.schedule(profile.route) AclSyncJob.schedule(profile.route)
changeState(State.CONNECTED) changeState(State.CONNECTED)
notification = new ShadowsocksNotification(this, profile.name, true)
} }
override def stopRunner(stopService: Boolean, msg: String = null) { override def createNotification() = new ShadowsocksNotification(this, profile.name, "service-nat", true)
if (notification != null) notification.destroy() override def stopRunner(stopService: Boolean, msg: String = null) {
// channge the state // channge the state
changeState(State.STOPPING) changeState(State.STOPPING)
......
...@@ -28,13 +28,15 @@ import android.os.{Build, PowerManager} ...@@ -28,13 +28,15 @@ import android.os.{Build, PowerManager}
import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationCompat.BigTextStyle import android.support.v4.app.NotificationCompat.BigTextStyle
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import android.support.v4.os.BuildCompat
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback.Stub import com.github.shadowsocks.aidl.IShadowsocksServiceCallback.Stub
import com.github.shadowsocks.utils.{Action, State, TrafficMonitor, Utils} import com.github.shadowsocks.utils.{Action, State, TrafficMonitor, Utils}
/** /**
* @author Mygod * @author Mygod
*/ */
class ShadowsocksNotification(private val service: BaseService, profileName: String, visible: Boolean = false) { class ShadowsocksNotification(private val service: BaseService, profileName: String,
channel: String, visible: Boolean = false) {
private val keyGuard = service.getSystemService(Context.KEYGUARD_SERVICE).asInstanceOf[KeyguardManager] private val keyGuard = service.getSystemService(Context.KEYGUARD_SERVICE).asInstanceOf[KeyguardManager]
private lazy val nm = service.getSystemService(Context.NOTIFICATION_SERVICE).asInstanceOf[NotificationManager] private lazy val nm = service.getSystemService(Context.NOTIFICATION_SERVICE).asInstanceOf[NotificationManager]
private lazy val callback = new Stub { private lazy val callback = new Stub {
...@@ -60,6 +62,7 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str ...@@ -60,6 +62,7 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
.setContentIntent(PendingIntent.getActivity(service, 0, new Intent(service, classOf[MainActivity]) .setContentIntent(PendingIntent.getActivity(service, 0, new Intent(service, classOf[MainActivity])
.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0)) .setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0))
.setSmallIcon(R.drawable.ic_stat_shadowsocks) .setSmallIcon(R.drawable.ic_stat_shadowsocks)
.setChannel(channel)
if (Build.VERSION.SDK_INT < 24) builder.addAction(R.drawable.ic_navigation_close, if (Build.VERSION.SDK_INT < 24) builder.addAction(R.drawable.ic_navigation_close,
service.getString(R.string.stop), PendingIntent.getBroadcast(service, 0, new Intent(Action.CLOSE), 0)) service.getString(R.string.stop), PendingIntent.getBroadcast(service, 0, new Intent(Action.CLOSE), 0))
private lazy val style = new BigTextStyle(builder) private lazy val style = new BigTextStyle(builder)
......
...@@ -26,6 +26,7 @@ import android.net.VpnService ...@@ -26,6 +26,7 @@ import android.net.VpnService
import android.os.{Bundle, Handler} import android.os.{Bundle, Handler}
import android.util.Log import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.Utils
object ShadowsocksRunnerActivity { object ShadowsocksRunnerActivity {
private final val TAG = "ShadowsocksRunnerActivity" private final val TAG = "ShadowsocksRunnerActivity"
...@@ -46,7 +47,7 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext { ...@@ -46,7 +47,7 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
def startBackgroundService() { def startBackgroundService() {
if (app.isNatEnabled) { if (app.isNatEnabled) {
bgService.use(app.profileId) Utils.startSsService(this)
finish() finish()
} else { } else {
val intent = VpnService.prepare(ShadowsocksRunnerActivity.this) val intent = VpnService.prepare(ShadowsocksRunnerActivity.this)
...@@ -73,7 +74,6 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext { ...@@ -73,7 +74,6 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
} else { } else {
attachService() attachService()
} }
finish()
} }
override def onDestroy() { override def onDestroy() {
...@@ -87,10 +87,7 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext { ...@@ -87,10 +87,7 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) { override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
resultCode match { resultCode match {
case Activity.RESULT_OK => case Activity.RESULT_OK => Utils.startSsService(this)
if (bgService != null) {
bgService.use(app.profileId)
}
case _ => case _ =>
Log.e(TAG, "Failed to start VpnService") Log.e(TAG, "Failed to start VpnService")
} }
......
...@@ -23,6 +23,7 @@ package com.github.shadowsocks ...@@ -23,6 +23,7 @@ package com.github.shadowsocks
import java.io.File import java.io.File
import java.util.Locale import java.util.Locale
import android.app.Service
import android.content._ import android.content._
import android.content.pm.PackageManager.NameNotFoundException import android.content.pm.PackageManager.NameNotFoundException
import android.net.VpnService import android.net.VpnService
...@@ -30,7 +31,6 @@ import android.os._ ...@@ -30,7 +31,6 @@ import android.os._
import android.util.Log import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.acl.{Acl, AclSyncJob, Subnet} import com.github.shadowsocks.acl.{Acl, AclSyncJob, Subnet}
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils._ import com.github.shadowsocks.utils._
import scala.collection.mutable.ArrayBuffer import scala.collection.mutable.ArrayBuffer
...@@ -42,7 +42,6 @@ class ShadowsocksVpnService extends VpnService with BaseService { ...@@ -42,7 +42,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
val PRIVATE_VLAN6 = "fdfe:dcba:9876::%s" val PRIVATE_VLAN6 = "fdfe:dcba:9876::%s"
var conn: ParcelFileDescriptor = _ var conn: ParcelFileDescriptor = _
var vpnThread: ShadowsocksVpnThread = _ var vpnThread: ShadowsocksVpnThread = _
private var notification: ShadowsocksNotification = _
var sslocalProcess: GuardedProcess = _ var sslocalProcess: GuardedProcess = _
var overtureProcess: GuardedProcess = _ var overtureProcess: GuardedProcess = _
...@@ -69,8 +68,6 @@ class ShadowsocksVpnService extends VpnService with BaseService { ...@@ -69,8 +68,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
vpnThread = null vpnThread = null
} }
if (notification != null) notification.destroy()
// channge the state // channge the state
changeState(State.STOPPING) changeState(State.STOPPING)
...@@ -103,20 +100,19 @@ class ShadowsocksVpnService extends VpnService with BaseService { ...@@ -103,20 +100,19 @@ class ShadowsocksVpnService extends VpnService with BaseService {
} }
} }
override def startRunner(profile: Profile) { override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = {
// ensure the VPNService is prepared // ensure the VPNService is prepared
if (VpnService.prepare(this) != null) { if (VpnService.prepare(this) != null) {
val i = new Intent(this, classOf[ShadowsocksRunnerActivity]) val i = new Intent(this, classOf[ShadowsocksRunnerActivity])
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(i) startActivity(i)
stopRunner(stopService = true) stopRunner(stopService = true)
return Service.START_NOT_STICKY
} } else super.onStartCommand(intent, flags, startId)
super.startRunner(profile)
} }
override def createNotification() = new ShadowsocksNotification(this, profile.name, "service-vpn")
override def connect() { override def connect() {
super.connect() super.connect()
...@@ -137,8 +133,6 @@ class ShadowsocksVpnService extends VpnService with BaseService { ...@@ -137,8 +133,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
if (profile.route != Acl.ALL && profile.route != Acl.CUSTOM_RULES) if (profile.route != Acl.ALL && profile.route != Acl.CUSTOM_RULES)
AclSyncJob.schedule(profile.route) AclSyncJob.schedule(profile.route)
notification = new ShadowsocksNotification(this, profile.name)
} }
/** Called when the activity is first created. */ /** Called when the activity is first created. */
......
...@@ -42,7 +42,7 @@ class TaskerActivity extends AppCompatActivity { ...@@ -42,7 +42,7 @@ class TaskerActivity extends AppCompatActivity {
typedArray.recycle() typedArray.recycle()
} }
private var item: Profile = _ private var item: Profile = _
private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[CheckedTextView] private val text = itemView.findViewById[CheckedTextView](android.R.id.text1)
itemView.setOnClickListener(this) itemView.setOnClickListener(this)
def bindDefault() { def bindDefault() {
......
...@@ -30,10 +30,17 @@ import com.github.shadowsocks.ShadowsocksApplication.app ...@@ -30,10 +30,17 @@ import com.github.shadowsocks.ShadowsocksApplication.app
class TaskerReceiver extends BroadcastReceiver { class TaskerReceiver extends BroadcastReceiver {
override def onReceive(context: Context, intent: Intent) { override def onReceive(context: Context, intent: Intent) {
val settings = TaskerSettings.fromIntent(intent) val settings = TaskerSettings.fromIntent(intent)
var changed = false
app.profileManager.getProfile(settings.profileId) match { app.profileManager.getProfile(settings.profileId) match {
case Some(_) => app.switchProfile(settings.profileId) case Some(_) => if (app.dataStore.profileId != settings.profileId) {
app.switchProfile(settings.profileId)
changed = true
}
case _ => case _ =>
} }
if (settings.switchOn) Utils.startSsService(context) else Utils.stopSsService(context) if (settings.switchOn) {
Utils.startSsService(context)
if (changed) Utils.reloadSsService(context)
} else Utils.stopSsService(context)
} }
} }
...@@ -30,7 +30,7 @@ class ToolbarFragment extends Fragment { ...@@ -30,7 +30,7 @@ class ToolbarFragment extends Fragment {
override def onViewCreated(view: View, savedInstanceState: Bundle) { override def onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
toolbar = view.findViewById(R.id.toolbar).asInstanceOf[Toolbar] toolbar = view.findViewById[Toolbar](R.id.toolbar)
val activity = getActivity.asInstanceOf[MainActivity] val activity = getActivity.asInstanceOf[MainActivity]
if (activity.crossfader == null) activity.drawer.setToolbar(activity, toolbar, true) if (activity.crossfader == null) activity.drawer.setToolbar(activity, toolbar, true)
} }
......
...@@ -46,8 +46,8 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi ...@@ -46,8 +46,8 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
private def createAclRuleDialog(text: CharSequence = "") = { private def createAclRuleDialog(text: CharSequence = "") = {
val view = getActivity.getLayoutInflater.inflate(R.layout.dialog_acl_rule, null) val view = getActivity.getLayoutInflater.inflate(R.layout.dialog_acl_rule, null)
val templateSelector = view.findViewById(R.id.template_selector).asInstanceOf[Spinner] val templateSelector = view.findViewById[Spinner](R.id.template_selector)
val editText = view.findViewById(R.id.content).asInstanceOf[EditText] val editText = view.findViewById[EditText](R.id.content)
PATTERN_DOMAIN.findFirstMatchIn(text) match { PATTERN_DOMAIN.findFirstMatchIn(text) match {
case Some(m) => case Some(m) =>
templateSelector.setSelection(DOMAIN) templateSelector.setSelection(DOMAIN)
...@@ -70,7 +70,7 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi ...@@ -70,7 +70,7 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
private final class AclRuleViewHolder(view: View) extends RecyclerView.ViewHolder(view) private final class AclRuleViewHolder(view: View) extends RecyclerView.ViewHolder(view)
with View.OnClickListener with View.OnLongClickListener { with View.OnClickListener with View.OnLongClickListener {
var item: AnyRef = _ var item: AnyRef = _
private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[TextView] private val text = itemView.findViewById[TextView](android.R.id.text1)
itemView.setOnClickListener(this) itemView.setOnClickListener(this)
itemView.setOnLongClickListener(this) itemView.setOnLongClickListener(this)
itemView.setBackgroundResource(R.drawable.background_selectable) itemView.setBackgroundResource(R.drawable.background_selectable)
...@@ -252,12 +252,11 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi ...@@ -252,12 +252,11 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
toolbar.setOnMenuItemClickListener(this) toolbar.setOnMenuItemClickListener(this)
selectionItem = toolbar.getMenu.findItem(R.id.selection) selectionItem = toolbar.getMenu.findItem(R.id.selection)
selectionItem.setVisible(selectedItems.nonEmpty) selectionItem.setVisible(selectedItems.nonEmpty)
list = view.findViewById(R.id.list).asInstanceOf[RecyclerView] list = view.findViewById(R.id.list)
list.setLayoutManager(new LinearLayoutManager(getActivity, LinearLayoutManager.VERTICAL, false)) list.setLayoutManager(new LinearLayoutManager(getActivity, LinearLayoutManager.VERTICAL, false))
list.setItemAnimator(new DefaultItemAnimator) list.setItemAnimator(new DefaultItemAnimator)
list.setAdapter(adapter) list.setAdapter(adapter)
val fastScroller = view.findViewById(R.id.fastscroller).asInstanceOf[FastScroller] view.findViewById[FastScroller](R.id.fastscroller).setRecyclerView(list)
fastScroller.setRecyclerView(list)
undoManager = new UndoSnackbarManager[AnyRef](getActivity.findViewById(R.id.snackbar), adapter.undo) undoManager = new UndoSnackbarManager[AnyRef](getActivity.findViewById(R.id.snackbar), adapter.undo)
new ItemTouchHelper(new SimpleCallback(0, ItemTouchHelper.START | ItemTouchHelper.END) { new ItemTouchHelper(new SimpleCallback(0, ItemTouchHelper.START | ItemTouchHelper.END) {
override def getSwipeDirs(recyclerView: RecyclerView, viewHolder: ViewHolder): Int = override def getSwipeDirs(recyclerView: RecyclerView, viewHolder: ViewHolder): Int =
......
...@@ -20,10 +20,14 @@ ...@@ -20,10 +20,14 @@
package com.github.shadowsocks.database package com.github.shadowsocks.database
import android.content.Context import java.nio.ByteBuffer
import android.content.{Context, SharedPreferences}
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.preference.PreferenceManager
import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.Key
import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper
import com.j256.ormlite.dao.Dao import com.j256.ormlite.dao.Dao
import com.j256.ormlite.support.ConnectionSource import com.j256.ormlite.support.ConnectionSource
...@@ -46,17 +50,20 @@ object DBHelper { ...@@ -46,17 +50,20 @@ object DBHelper {
} }
class DBHelper(val context: Context) class DBHelper(val context: Context)
extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 22) { extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 23) {
import DBHelper._ import DBHelper._
lazy val profileDao: Dao[Profile, Int] = getDao(classOf[Profile]) lazy val profileDao: Dao[Profile, Int] = getDao(classOf[Profile])
lazy val kvPairDao: Dao[KeyValuePair, String] = getDao(classOf[KeyValuePair])
def onCreate(database: SQLiteDatabase, connectionSource: ConnectionSource) { def onCreate(database: SQLiteDatabase, connectionSource: ConnectionSource) {
TableUtils.createTable(connectionSource, classOf[Profile]) TableUtils.createTable(connectionSource, classOf[Profile])
TableUtils.createTable(connectionSource, classOf[KeyValuePair])
} }
def recreate(database: SQLiteDatabase, connectionSource: ConnectionSource) { def recreate(database: SQLiteDatabase, connectionSource: ConnectionSource) {
TableUtils.dropTable(connectionSource, classOf[Profile], true) TableUtils.dropTable(connectionSource, classOf[Profile], true)
TableUtils.dropTable(connectionSource, classOf[KeyValuePair], true)
onCreate(database, connectionSource) onCreate(database, connectionSource)
} }
...@@ -132,6 +139,19 @@ class DBHelper(val context: Context) ...@@ -132,6 +139,19 @@ class DBHelper(val context: Context)
profileDao.executeRawNoArgs("DROP TABLE `tmp`;") profileDao.executeRawNoArgs("DROP TABLE `tmp`;")
profileDao.executeRawNoArgs("COMMIT;") profileDao.executeRawNoArgs("COMMIT;")
} }
if (oldVersion < 23) {
import KeyValuePair._
val old = PreferenceManager.getDefaultSharedPreferences(app)
kvPairDao.createOrUpdate(new KeyValuePair(Key.id, TYPE_INT,
ByteBuffer.allocate(4).putInt(old.getInt(Key.id, 0)).array()))
kvPairDao.createOrUpdate(new KeyValuePair(Key.isNAT, TYPE_BOOLEAN,
ByteBuffer.allocate(1).put((if (old.getBoolean(Key.isNAT, false)) 1 else 0).toByte).array()))
kvPairDao.createOrUpdate(new KeyValuePair(Key.tfo, TYPE_BOOLEAN,
ByteBuffer.allocate(1).put((if (old.getBoolean(Key.tfo, false)) 1 else 0).toByte).array()))
kvPairDao.createOrUpdate(new KeyValuePair(Key.currentVersionCode, TYPE_INT,
ByteBuffer.allocate(4).putInt(-1).array()))
}
} catch { } catch {
case ex: Exception => case ex: Exception =>
app.track(ex) app.track(ex)
......
package com.github.shadowsocks.database
import com.j256.ormlite.field.{DataType, DatabaseField}
/**
* @author Mygod
*/
object KeyValuePair {
val TYPE_UNINITIALIZED = 0
val TYPE_BOOLEAN = 1
val TYPE_FLOAT = 2
val TYPE_INT = 3
val TYPE_LONG = 4
val TYPE_STRING = 5
val TYPE_STRING_SET = 6
}
class KeyValuePair {
@DatabaseField(id = true)
var key: String = _
@DatabaseField
var valueType: Int = _
@DatabaseField(dataType = DataType.BYTE_ARRAY)
var value: Array[Byte] = _
def this(key: String, valueType: Int, value: Array[Byte]) = {
this()
this.key = key
this.valueType = valueType
this.value = value
}
}
...@@ -22,11 +22,11 @@ package com.github.shadowsocks.database ...@@ -22,11 +22,11 @@ package com.github.shadowsocks.database
import java.util.Locale import java.util.Locale
import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import android.os.Binder import android.os.Binder
import android.util.Base64 import android.util.Base64
import com.github.shadowsocks.plugin.PluginConfiguration import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.preference.OrmLitePreferenceDataStore
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
import com.j256.ormlite.field.{DataType, DatabaseField} import com.j256.ormlite.field.{DataType, DatabaseField}
...@@ -108,37 +108,38 @@ class Profile { ...@@ -108,37 +108,38 @@ class Profile {
} }
override def toString: String = toUri.toString override def toString: String = toUri.toString
def serialize(editor: SharedPreferences.Editor): SharedPreferences.Editor = editor def serialize(store: OrmLitePreferenceDataStore) {
.putString(Key.name, name) store.putString(Key.name, name)
.putString(Key.host, host) store.putString(Key.host, host)
.putInt(Key.localPort, localPort) store.putInt(Key.localPort, localPort)
.putInt(Key.remotePort, remotePort) store.putInt(Key.remotePort, remotePort)
.putString(Key.password, password) store.putString(Key.password, password)
.putString(Key.route, route) store.putString(Key.route, route)
.putString(Key.remoteDns, remoteDns) store.putString(Key.remoteDns, remoteDns)
.putString(Key.method, method) store.putString(Key.method, method)
.putBoolean(Key.proxyApps, proxyApps) store.proxyApps = proxyApps
.putBoolean(Key.bypass, bypass) store.bypass = bypass
.putBoolean(Key.udpdns, udpdns) store.putBoolean(Key.udpdns, udpdns)
.putBoolean(Key.ipv6, ipv6) store.putBoolean(Key.ipv6, ipv6)
.putString(Key.individual, individual) store.individual = individual
.putString(Key.plugin, plugin) store.plugin = plugin
.remove(Key.dirty) store.remove(Key.dirty)
def deserialize(pref: SharedPreferences) { }
def deserialize(store: OrmLitePreferenceDataStore) {
// It's assumed that default values are never used, so 0/false/null is always used even if that isn't the case // It's assumed that default values are never used, so 0/false/null is always used even if that isn't the case
name = pref.getString(Key.name, null) name = store.getString(Key.name, null)
host = pref.getString(Key.host, null) host = store.getString(Key.host, null)
localPort = pref.getInt(Key.localPort, 0) localPort = store.getInt(Key.localPort, 0)
remotePort = pref.getInt(Key.remotePort, 0) remotePort = store.getInt(Key.remotePort, 0)
password = pref.getString(Key.password, null) password = store.getString(Key.password, null)
method = pref.getString(Key.method, null) method = store.getString(Key.method, null)
route = pref.getString(Key.route, null) route = store.getString(Key.route, null)
remoteDns = pref.getString(Key.remoteDns, null) remoteDns = store.getString(Key.remoteDns, null)
proxyApps = pref.getBoolean(Key.proxyApps, false) proxyApps = store.proxyApps
bypass = pref.getBoolean(Key.bypass, false) bypass = store.bypass
udpdns = pref.getBoolean(Key.udpdns, false) udpdns = store.getBoolean(Key.udpdns, false)
ipv6 = pref.getBoolean(Key.ipv6, false) ipv6 = store.getBoolean(Key.ipv6, false)
individual = pref.getString(Key.individual, null) individual = store.individual
plugin = pref.getString(Key.plugin, null) plugin = store.plugin
} }
} }
...@@ -46,9 +46,9 @@ final class BottomSheetPreferenceDialogFragment extends PreferenceDialogFragment ...@@ -46,9 +46,9 @@ final class BottomSheetPreferenceDialogFragment extends PreferenceDialogFragment
private final class IconListViewHolder(val dialog: BottomSheetDialog, view: View) extends ViewHolder(view) private final class IconListViewHolder(val dialog: BottomSheetDialog, view: View) extends ViewHolder(view)
with View.OnClickListener with View.OnLongClickListener { with View.OnClickListener with View.OnLongClickListener {
private var index: Int = _ private var index: Int = _
private val text1 = view.findViewById(android.R.id.text1).asInstanceOf[TextView] private val text1 = view.findViewById[TextView](android.R.id.text1)
private val text2 = view.findViewById(android.R.id.text2).asInstanceOf[TextView] private val text2 = view.findViewById[TextView](android.R.id.text2)
private val icon = view.findViewById(android.R.id.icon).asInstanceOf[ImageView] private val icon = view.findViewById[ImageView](android.R.id.icon)
view.setOnClickListener(this) view.setOnClickListener(this)
view.setOnLongClickListener(this) view.setOnLongClickListener(this)
......
package com.github.shadowsocks.preference
import android.support.v7.preference.PreferenceDataStore
/**
* @author Mygod
*/
trait OnPreferenceDataStoreChangeListener {
def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String)
}
package com.github.shadowsocks.preference
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
import java.util
import android.support.v7.preference.PreferenceDataStore
import com.github.shadowsocks.database.{DBHelper, KeyValuePair}
import com.github.shadowsocks.utils.Key
import scala.collection.JavaConversions._
/**
* @author Mygod
*/
//noinspection AccessorLikeMethodIsUnit
final class OrmLitePreferenceDataStore(dbHelper: DBHelper) extends PreferenceDataStore {
import KeyValuePair._
override def getBoolean(key: String, defValue: Boolean = false): Boolean = dbHelper.kvPairDao.queryForId(key) match {
case pair: KeyValuePair =>
if (pair.valueType == TYPE_BOOLEAN) ByteBuffer.wrap(pair.value).get() != 0 else defValue
case _ => defValue
}
override def getFloat(key: String, defValue: Float): Float = dbHelper.kvPairDao.queryForId(key) match {
case pair: KeyValuePair =>
if (pair.valueType == TYPE_FLOAT) ByteBuffer.wrap(pair.value).getFloat() else defValue
case _ => defValue
}
override def getInt(key: String, defValue: Int): Int = dbHelper.kvPairDao.queryForId(key) match {
case pair: KeyValuePair =>
if (pair.valueType == TYPE_INT) ByteBuffer.wrap(pair.value).getInt() else defValue
case _ => defValue
}
override def getLong(key: String, defValue: Long): Long = dbHelper.kvPairDao.queryForId(key) match {
case pair: KeyValuePair =>
if (pair.valueType == TYPE_LONG) ByteBuffer.wrap(pair.value).getLong() else defValue
case _ => defValue
}
override def getString(key: String, defValue: String = null): String = dbHelper.kvPairDao.queryForId(key) match {
case pair: KeyValuePair =>
if (pair.valueType == TYPE_STRING) new String(pair.value) else defValue
case _ => defValue
}
override def getStringSet(key: String, defValue: util.Set[String]): util.Set[String] =
dbHelper.kvPairDao.queryForId(key) match {
case pair: KeyValuePair => if (pair.valueType == TYPE_STRING_SET) {
val buffer = ByteBuffer.wrap(pair.value)
val result = new util.HashSet[String]()
while (buffer.hasRemaining) {
val chArr = new Array[Byte](buffer.getInt)
buffer.get(chArr)
result.add(new String(chArr))
}
result
} else defValue
case _ => defValue
}
override def putBoolean(key: String, value: Boolean) {
dbHelper.kvPairDao.createOrUpdate(
new KeyValuePair(key, TYPE_BOOLEAN, ByteBuffer.allocate(1).put((if (value) 1 else 0).toByte).array()))
fireChangeListener(key)
}
override def putFloat(key: String, value: Float) {
dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_FLOAT, ByteBuffer.allocate(4).putFloat(value).array()))
fireChangeListener(key)
}
override def putInt(key: String, value: Int) {
dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_INT, ByteBuffer.allocate(4).putInt(value).array()))
fireChangeListener(key)
}
override def putLong(key: String, value: Long) {
dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_LONG, ByteBuffer.allocate(8).putLong(value).array()))
fireChangeListener(key)
}
override def putString(key: String, value: String) {
value match {
case null => remove(key)
case _ => dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_STRING, value.getBytes()))
}
fireChangeListener(key)
}
override def putStringSet(key: String, value: util.Set[String]) {
val stream = new ByteArrayOutputStream()
for (v <- value) {
stream.write(ByteBuffer.allocate(4).putInt(v.length).array())
stream.write(v.getBytes())
}
dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_STRING_SET, stream.toByteArray))
fireChangeListener(key)
}
def remove(key: String): Int = dbHelper.kvPairDao.deleteById(key)
private var listeners: Set[OnPreferenceDataStoreChangeListener] = Set.empty
private def fireChangeListener(key: String) = listeners.foreach(_.onPreferenceDataStoreChanged(this, key))
def registerChangeListener(listener: OnPreferenceDataStoreChangeListener): Unit = listeners += listener
def unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener): Unit = listeners -= listener
def profileId: Int = getInt(Key.id, 0)
def profileId_=(i: Int): Unit = putInt(Key.id, i)
def isNAT: Boolean = getBoolean(Key.isNAT)
def isNAT_=(value: Boolean): Unit = putBoolean(Key.isNAT, value)
def proxyApps: Boolean = getBoolean(Key.proxyApps)
def proxyApps_=(value: Boolean): Unit = putBoolean(Key.proxyApps, value)
def bypass: Boolean = getBoolean(Key.bypass)
def bypass_=(value: Boolean): Unit = putBoolean(Key.bypass, value)
def individual: String = getString(Key.individual)
def individual_=(value: String): Unit = putString(Key.individual, value)
def plugin: String = getString(Key.plugin)
def plugin_=(value: String): Unit = putString(Key.plugin, value)
def dirty: Boolean = getBoolean(Key.dirty)
def dirty_=(value: Boolean): Unit = putBoolean(Key.dirty, value)
}
...@@ -94,6 +94,7 @@ object State { ...@@ -94,6 +94,7 @@ object State {
object Action { object Action {
final val SERVICE = "com.github.shadowsocks.SERVICE" final val SERVICE = "com.github.shadowsocks.SERVICE"
final val CLOSE = "com.github.shadowsocks.CLOSE" final val CLOSE = "com.github.shadowsocks.CLOSE"
final val RELOAD = "com.github.shadowsocks.RELOAD"
final val EXTRA_PROFILE_ID = "com.github.shadowsocks.EXTRA_PROFILE_ID" final val EXTRA_PROFILE_ID = "com.github.shadowsocks.EXTRA_PROFILE_ID"
} }
...@@ -33,7 +33,7 @@ import android.view.View.MeasureSpec ...@@ -33,7 +33,7 @@ import android.view.View.MeasureSpec
import android.view.{Gravity, View, Window} import android.view.{Gravity, View, Window}
import android.widget.Toast import android.widget.Toast
import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.{BuildConfig, ShadowsocksRunnerService} import com.github.shadowsocks.{BuildConfig, ShadowsocksNatService, ShadowsocksVpnService}
import org.xbill.DNS._ import org.xbill.DNS._
import scala.collection.JavaConversions._ import scala.collection.JavaConversions._
...@@ -137,10 +137,11 @@ object Utils { ...@@ -137,10 +137,11 @@ object Utils {
} }
def startSsService(context: Context) { def startSsService(context: Context) {
val intent = new Intent(context, classOf[ShadowsocksRunnerService]) val intent =
context.startService(intent) new Intent(context, if (app.dataStore.isNAT) classOf[ShadowsocksNatService] else classOf[ShadowsocksVpnService])
if (Build.VERSION.SDK_INT >= 26) context.startForegroundService(intent) else context.startService(intent)
} }
def reloadSsService(context: Context): Unit = context.sendBroadcast(new Intent(Action.RELOAD))
def stopSsService(context: Context) { def stopSsService(context: Context) {
val intent = new Intent(Action.CLOSE) val intent = new Intent(Action.CLOSE)
context.sendBroadcast(intent) context.sendBroadcast(intent)
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
package="com.github.shadowsocks.plugin"> package="com.github.shadowsocks.plugin">
<uses-sdk <uses-sdk
android:minSdkVersion="19" android:minSdkVersion="19"
android:targetSdkVersion="25"/> android:targetSdkVersion="26"/>
<application <application
android:theme="@style/Theme.Shadowsocks"> android:theme="@style/Theme.Shadowsocks">
<meta-data android:name="com.github.shadowsocks.plugin.version" <meta-data android:name="com.github.shadowsocks.plugin.version"
......
...@@ -40,6 +40,6 @@ ...@@ -40,6 +40,6 @@
<item name="android:scrollbars">vertical</item> <item name="android:scrollbars">vertical</item>
</style> </style>
<style name="NumberPickerStyle"> <style name="NumberPickerStyle">
<item name="android:textSize">14sp</item> <item name="android:textSize">18sp</item>
</style> </style>
</resources> </resources>
...@@ -36,7 +36,7 @@ class EditTextPreferenceDialogFragment extends PreferenceDialogFragment { ...@@ -36,7 +36,7 @@ class EditTextPreferenceDialogFragment extends PreferenceDialogFragment {
val oldParent = editText.getParent.asInstanceOf[ViewGroup] val oldParent = editText.getParent.asInstanceOf[ViewGroup]
if (oldParent eq view) return if (oldParent eq view) return
if (oldParent != null) oldParent.removeView(editText) if (oldParent != null) oldParent.removeView(editText)
val oldEdit = view.findViewById(android.R.id.edit) val oldEdit = view.findViewById[View](android.R.id.edit)
if (oldEdit == null) return if (oldEdit == null) return
val container = oldEdit.getParent.asInstanceOf[ViewGroup] val container = oldEdit.getParent.asInstanceOf[ViewGroup]
if (container == null) return if (container == null) return
......
...@@ -77,7 +77,7 @@ class PreferenceGroupAdapter(group: PreferenceGroup) extends Old(group) { ...@@ -77,7 +77,7 @@ class PreferenceGroupAdapter(group: PreferenceGroup) extends Old(group) {
view.setBackground(background) view.setBackground(background)
ViewCompat.setPaddingRelative(view, s, t, e, b) ViewCompat.setPaddingRelative(view, s, t, e, b)
} }
val widgetFrame = view.findViewById(android.R.id.widget_frame).asInstanceOf[ViewGroup] val widgetFrame = view.findViewById[ViewGroup](android.R.id.widget_frame)
if (widgetFrame != null) { if (widgetFrame != null) {
val widgetResId = fieldWidgetResId.get(pl).asInstanceOf[Int] val widgetResId = fieldWidgetResId.get(pl).asInstanceOf[Int]
if (widgetResId != 0) inflater.inflate(widgetResId, widgetFrame) else widgetFrame.setVisibility(View.GONE) if (widgetResId != 0) inflater.inflate(widgetResId, widgetFrame) else widgetFrame.setVisibility(View.GONE)
......
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