Commit f5247d62 authored by Max Lv's avatar Max Lv

Bump version

parents f6f4f45f f19f8d8f
......@@ -5,7 +5,7 @@ jdk:
env:
global:
- NDK_VERSION=r14
- NDK_VERSION=r15b
- NDK_CCACHE=ccache
- GOROOT_BOOTSTRAP=$GOROOT
- ANDROID_NDK_HOME=$HOME/.android/android-ndk-${NDK_VERSION}
......@@ -13,7 +13,7 @@ env:
- PATH=${ANDROID_NDK_HOME}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools:${PATH}
scala:
- 2.11.8
- 2.11.11
before_cache:
- find $HOME/.sbt -name "*.lock" | xargs rm
......@@ -27,7 +27,7 @@ cache:
android:
components:
- tools
- build-tools-25.0.2
- build-tools-26.0.0
- extra-android-m2repository
- extra-google-m2repository
......
......@@ -14,9 +14,9 @@ A [shadowsocks](http://shadowsocks.org) client for Android, written in Scala.
* SBT 0.13.0+
* Go 1.4+
* Android SDK
- Build Tools 25+
- Build Tools 26+
- Android Support Repository and Google Repository (see `build.sbt` for version)
- Android NDK r14+
- Android NDK r15+
### BUILD
......
lazy val commonSettings = Seq(
scalaVersion := "2.11.9",
scalaVersion := "2.11.11",
dexMaxHeap := "4g",
organization := "com.github.shadowsocks",
platformTarget := "android-25",
platformTarget := "android-26",
compileOrder := CompileOrder.JavaThenScala,
javacOptions ++= "-source" :: "1.7" :: "-target" :: "1.7" :: Nil,
......@@ -18,10 +18,12 @@ lazy val commonSettings = Seq(
shrinkResources := true,
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"
lazy val root = Project(id = "shadowsocks-android", base = file("."))
.settings(commonSettings)
.aggregate(plugin, mobile)
......
......@@ -4,8 +4,8 @@ enablePlugins(AndroidGms)
android.useSupportVectors
name := "shadowsocks"
version := "4.1.8"
versionCode := Some(188)
version := "4.2.0"
versionCode := Some(190)
proguardOptions ++=
"-dontwarn com.google.android.gms.internal.**" ::
......@@ -23,11 +23,11 @@ proguardOptions ++=
"-keep public class com.evernote.android.job.JobRescheduleService" ::
Nil
val playServicesVersion = "10.2.6"
val playServicesVersion = "11.0.4"
resolvers += Resolver.jcenterRepo
libraryDependencies ++=
"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.google.android.gms" % "play-services-ads" % playServicesVersion ::
"com.google.android.gms" % "play-services-analytics" % playServicesVersion ::
......@@ -35,15 +35,15 @@ libraryDependencies ++=
"com.google.firebase" % "firebase-config" % playServicesVersion ::
"com.j256.ormlite" % "ormlite-android" % "5.0" ::
"com.mikepenz" % "crossfader" % "1.5.0" ::
"com.mikepenz" % "fastadapter" % "2.5.2" ::
"com.mikepenz" % "iconics-core" % "2.8.4" ::
"com.mikepenz" % "materialdrawer" % "5.9.1" ::
"com.mikepenz" % "materialize" % "1.0.1" ::
"com.squareup.okhttp3" % "okhttp" % "3.8.0" ::
"com.mikepenz" % "fastadapter" % "2.6.2" ::
"com.mikepenz" % "iconics-core" % "2.8.9" ::
"com.mikepenz" % "materialdrawer" % "5.9.4" ::
"com.mikepenz" % "materialize" % "1.0.3" ::
"com.squareup.okhttp3" % "okhttp" % "3.8.1" ::
"com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" ::
"dnsjava" % "dnsjava" % "2.1.8" ::
"eu.chainfire" % "libsuperuser" % "1.0.0.201704021214" ::
"me.dm7.barcodescanner" % "zxing" % "1.9.3" ::
"me.dm7.barcodescanner" % "zxing" % "1.9.5" ::
"net.glxn.qrgen" % "android" % "2.0" ::
Nil
......
......@@ -17,13 +17,17 @@
<uses-sdk
android:minSdkVersion="19"
android:targetSdkVersion="25"/>
android:targetSdkVersion="26"/>
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
android:value="true" />
<application
android:name=".ShadowsocksApplication"
android:icon="@mipmap/ic_launcher"
android:backupAgent=".ShadowsocksBackupAgent"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:banner="@drawable/ic_start_connected">
<meta-data android:name="com.google.android.gms.version"
......@@ -96,18 +100,14 @@
android:label="@string/quick_toggle"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:excludeFromRecents="true"
android:taskAffinity=""
android:taskAffinity=""
android:process=":bg"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" />
</intent-filter>
</activity>
<service
android:name=".ShadowsocksRunnerService"
android:exported="false">
</service>
<service
android:name=".ShadowsocksNatService"
android:process=":bg"
......@@ -134,13 +134,13 @@
</intent-filter>
</service>
<receiver android:name=".BootReceiver" android:enabled="false">
<receiver android:name=".BootReceiver" android:process=":bg" android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
<receiver android:name=".TaskerReceiver" tools:ignore="ExportedReceiver">
<receiver android:name=".TaskerReceiver" android:process=":bg" tools:ignore="ExportedReceiver">
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING"/>
</intent-filter>
......@@ -159,9 +159,6 @@
android:process=":bg"/>
<receiver android:name="com.evernote.android.job.JobBootReceiver"
android:process=":bg"/>
<!-- Strip useless components -->
<activity android:name="com.google.android.gms.ads.purchase.InAppPurchaseActivity" tools:node="remove"/>
</application>
</manifest>
......@@ -10,7 +10,4 @@ interface IShadowsocksService {
oneway void startListeningForBandwidth(IShadowsocksServiceCallback cb);
oneway void stopListeningForBandwidth(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
......@@ -16,24 +16,18 @@
package com.github.shadowsocks.utils;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.annotation.TargetApi;
import android.util.Log;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.LinkProperties;
import android.os.Build;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.net.InetAddress;
public class Dns {
public static String getDnsResolver(Context context) throws Exception {
Collection<InetAddress> dnsResolvers = getDnsResolvers(context);
if (dnsResolvers.isEmpty()) {
......@@ -46,25 +40,22 @@ public class Dns {
return dnsResolver;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static Collection<InetAddress> getDnsResolvers(Context context) throws Exception {
ArrayList<InetAddress> addresses = new ArrayList<InetAddress>();
ConnectivityManager connectivityManager =
(ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
Class<?> LinkPropertiesClass = Class.forName("android.net.LinkProperties");
Method getActiveLinkPropertiesMethod = ConnectivityManager.class.getMethod("getActiveLinkProperties", new Class []{});
Method getActiveLinkPropertiesMethod = ConnectivityManager.class.getMethod("getActiveLinkProperties");
Object linkProperties = getActiveLinkPropertiesMethod.invoke(connectivityManager);
if (linkProperties != null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
Method getDnsesMethod = LinkPropertiesClass.getMethod("getDnses", new Class []{});
Method getDnsesMethod = LinkPropertiesClass.getMethod("getDnses");
Collection<?> dnses = (Collection<?>)getDnsesMethod.invoke(linkProperties);
for (Object dns : dnses) {
addresses.add((InetAddress)dns);
}
} else {
for (InetAddress dns : ((LinkProperties)linkProperties).getDnsServers()) {
addresses.add(dns);
}
addresses.addAll(((LinkProperties) linkProperties).getDnsServers());
}
}
......
<?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>
......@@ -37,7 +37,7 @@
<android.support.v7.widget.AppCompatImageView android:id="@+id/share"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
android:padding="8dp"
android:layout_gravity="top"
app:srcCompat="@drawable/ic_social_share"
android:background="?attr/selectableItemBackgroundBorderless"
......@@ -47,7 +47,7 @@
<android.support.v7.widget.AppCompatImageView android:id="@+id/edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
android:padding="8dp"
android:layout_gravity="top"
app:srcCompat="@drawable/ic_image_edit"
android:background="?attr/selectableItemBackgroundBorderless"
......
<?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>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#7488A1</color>
</resources>
......@@ -56,6 +56,8 @@
<string name="udp_dns_summary">Forward all DNS requests to remote</string>
<!-- 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="invalid_server">Invalid server name</string>
<string name="service_failed">Failed to connect the remote server</string>
......
<?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 {
override def onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
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.setWebViewClient(new WebViewClient() {
override def shouldOverrideUrlLoading(view: WebView, url: String): Boolean = {
......
......@@ -70,8 +70,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
import AppManager._
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 check = itemView.findViewById(R.id.itemcheck).asInstanceOf[Switch]
private val icon = itemView.findViewById[ImageView](R.id.itemicon)
private val check = itemView.findViewById[Switch](R.id.itemcheck)
private var item: ProxiedApp = _
itemView.setOnClickListener(this)
......@@ -92,7 +92,10 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
proxiedApps.add(item.packageName)
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 {
private val appsLoading = new AtomicBoolean
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]
override def onDestroy() {
......@@ -136,7 +139,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
case R.id.action_apply_all =>
app.profileManager.getAllProfiles match {
case Some(profiles) =>
val proxiedAppString = app.settings.getString(Key.individual, null)
val proxiedAppString = app.dataStore.individual
profiles.foreach(p => {
p.individual = proxiedAppString
app.profileManager.updateProfile(p)
......@@ -146,10 +149,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
}
return true
case R.id.action_export =>
val bypass = app.settings.getBoolean(Key.bypass, false)
val proxiedAppString = app.settings.getString(Key.individual, null)
val clip = ClipData.newPlainText(Key.individual, bypass + "\n" + proxiedAppString)
clipboard.setPrimaryClip(clip)
clipboard.setPrimaryClip(ClipData.newPlainText(Key.individual,
app.dataStore.bypass + "\n" + app.dataStore.individual))
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
return true
case R.id.action_import =>
......@@ -163,7 +164,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
val (enabled, apps) = if (i < 0) (proxiedAppString, "")
else (proxiedAppString.substring(0, i), proxiedAppString.substring(i + 1))
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()
appListView.setVisibility(View.GONE)
fastScroller.setVisibility(View.GONE)
......@@ -202,18 +204,23 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
toolbar.inflateMenu(R.menu.app_manager_menu)
toolbar.setOnMenuItemClickListener(this)
if (!app.settings.getBoolean(Key.proxyApps, false))
app.editor.putBoolean(Key.proxyApps, true).putBoolean(Key.dirty, true).apply()
if (!app.dataStore.proxyApps) {
app.dataStore.proxyApps = true
app.dataStore.dirty = true
}
findViewById(R.id.onSwitch).asInstanceOf[Switch]
.setOnCheckedChangeListener((_, checked) => {
app.editor.putBoolean(Key.proxyApps, checked).putBoolean(Key.dirty, true).apply()
app.dataStore.proxyApps = checked
app.dataStore.dirty = true
finish()
})
bypassSwitch = findViewById(R.id.bypassSwitch).asInstanceOf[Switch]
bypassSwitch.setChecked(app.settings.getBoolean(Key.bypass, false))
bypassSwitch.setOnCheckedChangeListener((_, checked) =>
app.editor.putBoolean(Key.bypass, checked).putBoolean(Key.dirty, true).apply())
bypassSwitch.setChecked(app.dataStore.bypass)
bypassSwitch.setOnCheckedChangeListener((_, checked) => {
app.dataStore.bypass = checked
app.dataStore.dirty = true
})
initProxiedApps()
loadingView = findViewById(R.id.loading)
......
......@@ -21,15 +21,14 @@
package com.github.shadowsocks
import java.io.{File, IOException}
import java.net.{Inet6Address, InetAddress}
import java.util
import java.util.concurrent.TimeUnit
import java.util.{Timer, TimerTask}
import java.net.InetAddress
import java.net.Inet6Address
import android.app.Service
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
import android.os.{Handler, IBinder, RemoteCallbackList}
import android.content._
import android.os.{Build, Handler, IBinder, RemoteCallbackList}
import android.text.TextUtils
import android.util.Log
import android.widget.Toast
......@@ -65,9 +64,12 @@ trait BaseService extends Service {
lazy val handler = new Handler(getMainLooper)
lazy val restartHanlder = new Handler(getMainLooper)
private val closeReceiver: BroadcastReceiver = (context: Context, _: Intent) => {
Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show()
stopRunner(stopService = true)
private var notification: ShadowsocksNotification = _
private val closeReceiver: BroadcastReceiver = (context: Context, intent: Intent) => intent.getAction match {
case Action.RELOAD => forceLoad()
case _ =>
Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show()
stopRunner(stopService = true)
}
var closeReceiverRegistered: Boolean = _
......@@ -103,20 +105,6 @@ trait BaseService extends Service {
stopListeningForBandwidth(cb) // saves an RPC, and safer
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)) {
......@@ -124,9 +112,18 @@ trait BaseService extends Service {
false
} else true
def connect() {
profile.name = profile.getName // save original name before it's (possibly) overwritten by IP addresses
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() {
if (profile.host == "198.199.101.152") {
val client = new OkHttpClient.Builder()
.dns(hostname => Utils.resolve(hostname, enableIPv6 = false) match {
......@@ -160,36 +157,9 @@ trait BaseService extends Service {
pluginPath = PluginManager.init(plugin)
}
def startRunner(profile: Profile) {
this.profile = profile
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 createNotification(): ShadowsocksNotification
def startRunner(): Unit = if (Build.VERSION.SDK_INT >= 26) startForegroundService(new Intent(this, getClass))
else startService(new Intent(this, getClass))
def stopRunner(stopService: Boolean, msg: String = null) {
// clean up recevier
......@@ -198,6 +168,8 @@ trait BaseService extends Service {
closeReceiverRegistered = false
}
if (notification != null) notification.destroy()
// Make sure update total traffic when stopping the runner
updateTrafficTotal(TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
......@@ -276,7 +248,49 @@ trait BaseService extends Service {
}
// 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
if (profile == null) {
stopRunner(stopService = true)
return Service.START_NOT_STICKY
}
profile.name = profile.getName // save original name before it's (possibly) overwritten by IP addresses
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) {
val handler = new Handler(getMainLooper)
......
......@@ -20,27 +20,24 @@
package com.github.shadowsocks
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v14.preference.SwitchPreference
import android.support.v7.preference.PreferenceDataStore
import be.mygod.preference.PreferenceFragment
import com.github.shadowsocks.utils.{Key, TcpFastOpen}
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) {
getPreferenceManager.setPreferenceDataStore(app.dataStore)
addPreferencesFromResource(R.xml.pref_global)
val switch = findPreference(Key.isAutoConnect).asInstanceOf[SwitchPreference]
switch.setOnPreferenceChangeListener((_, value) => {
BootReceiver.setEnabled(getActivity, value.asInstanceOf[Boolean])
true
})
if (getPreferenceManager.getSharedPreferences.getBoolean(Key.isAutoConnect, false)) {
BootReceiver.setEnabled(getActivity, true)
getPreferenceManager.getSharedPreferences.edit.remove(Key.isAutoConnect).apply()
}
switch.setChecked(BootReceiver.getEnabled(getActivity))
val tfo = findPreference(Key.tfo).asInstanceOf[SwitchPreference]
......@@ -56,16 +53,16 @@ class GlobalConfigFragment extends PreferenceFragment with OnSharedPreferenceCha
tfo.setEnabled(false)
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() {
app.settings.unregisterOnSharedPreferenceChangeListener(this)
app.dataStore.unregisterChangeListener(this)
super.onDestroy()
}
def onSharedPreferenceChanged(pref: SharedPreferences, key: String): Unit = key match {
case Key.isNAT => findPreference(key).asInstanceOf[SwitchPreference].setChecked(pref.getBoolean(key, false))
def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String): Unit = key match {
case Key.isNAT => findPreference(key).asInstanceOf[SwitchPreference].setChecked(store.getBoolean(key, false))
case _ =>
}
}
......@@ -26,7 +26,6 @@ import java.util.Locale
import android.app.backup.BackupManager
import android.app.{Activity, ProgressDialog}
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.content._
import android.net.{Uri, VpnService}
import android.nfc.{NdefMessage, NfcAdapter}
......@@ -36,6 +35,7 @@ import android.support.design.widget.{FloatingActionButton, Snackbar}
import android.support.v4.content.ContextCompat
import android.support.v7.app.AlertDialog
import android.support.v7.content.res.AppCompatResources
import android.support.v7.preference.PreferenceDataStore
import android.support.v7.widget.RecyclerView.ViewHolder
import android.text.TextUtils
import android.util.Log
......@@ -45,6 +45,7 @@ import com.github.jorgecastilloprz.FABProgressCircle
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.acl.{Acl, CustomRulesFragment}
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.utils.CloseUtils.autoDisconnect
import com.github.shadowsocks.utils._
import com.mikepenz.crossfader.Crossfader
......@@ -67,7 +68,7 @@ object MainActivity {
}
class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawerItemClickListener
with OnSharedPreferenceChangeListener {
with OnPreferenceDataStoreChangeListener {
import MainActivity._
// UI
......@@ -165,7 +166,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
override def onServiceDisconnected(): Unit = changeState(State.IDLE)
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(() => {
detachService()
......@@ -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 {
case Activity.RESULT_OK => bgService.use(app.profileId)
case Activity.RESULT_OK => Utils.startSsService(this)
case _ => Log.e(TAG, "Failed to start VpnService")
}
......@@ -246,7 +247,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
} else drawer = drawerBuilder.build()
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")
if (tf != null) title.setTypeface(tf)
......@@ -256,7 +257,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
txRateText = findViewById(R.id.txRate).asInstanceOf[TextView]
rxText = findViewById(R.id.rx).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
statusText.setText(R.string.connection_test_testing)
val id = testCount // it would change by other code
......@@ -298,8 +299,8 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
fab = findViewById(R.id.fab).asInstanceOf[FloatingActionButton]
fabProgressCircle = findViewById(R.id.fabProgressCircle).asInstanceOf[FABProgressCircle]
fab.setOnClickListener(_ => if (state == State.CONNECTED) bgService.use(-1) else Utils.ThrowableFuture {
if (app.isNatEnabled) bgService.use(app.profileId) else {
fab.setOnClickListener(_ => if (state == State.CONNECTED) Utils.stopSsService(this) else Utils.ThrowableFuture {
if (app.isNatEnabled) Utils.startSsService(this) else {
val intent = VpnService.prepare(this)
if (intent != null) startActivityForResult(intent, REQUEST_CONNECT)
else handler.post(() => onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null))
......@@ -313,7 +314,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
changeState(State.IDLE) // reset everything to init state
handler.post(() => attachService(callback))
app.settings.registerOnSharedPreferenceChangeListener(this)
app.dataStore.registerChangeListener(this)
val intent = getIntent
if (intent != null) handleShareIntent(intent)
......@@ -350,7 +351,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
.show()
}
def onSharedPreferenceChanged(pref: SharedPreferences, key: String): Unit = key match {
def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String): Unit = key match {
case Key.isNAT => handler.post(() => {
detachService()
attachService(callback)
......@@ -368,7 +369,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
case DRAWER_PROFILES => displayFragment(new ProfilesFragment)
case DRAWER_RECOVERY =>
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 handler = new Handler {
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
override def onDestroy() {
super.onDestroy()
app.settings.unregisterOnSharedPreferenceChangeListener(this)
app.dataStore.unregisterChangeListener(this)
detachService()
new BackupManager(this).dataChanged()
handler.removeCallbacksAndMessages(null)
......
......@@ -49,7 +49,7 @@ class ProfileConfigActivity extends Activity {
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)
.setPositiveButton(R.string.yes, ((_, _) => child.saveAndExit()): DialogInterface.OnClickListener)
.setNegativeButton(R.string.no, ((_, _) => finish()): DialogInterface.OnClickListener)
......
......@@ -21,13 +21,12 @@
package com.github.shadowsocks
import android.app.Activity
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.content._
import android.os.{Build, Bundle, UserManager}
import android.support.design.widget.Snackbar
import android.support.v14.preference.SwitchPreference
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.text.TextUtils
import android.view.MenuItem
......@@ -35,7 +34,7 @@ import be.mygod.preference.{EditTextPreference, PreferenceFragment}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.database.Profile
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}
object ProfileConfigFragment {
......@@ -43,7 +42,7 @@ object ProfileConfigFragment {
}
class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListener
with OnSharedPreferenceChangeListener {
with OnPreferenceDataStoreChangeListener {
import ProfileConfigFragment._
private var profile: Profile = _
......@@ -53,10 +52,11 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
private var pluginConfiguration: PluginConfiguration = _
override def onCreatePreferences(bundle: Bundle, key: String) {
getPreferenceManager.setPreferenceDataStore(app.dataStore)
app.profileManager.getProfile(getActivity.getIntent.getIntExtra(Action.EXTRA_PROFILE_ID, -1)) match {
case Some(p) =>
profile = p
profile.serialize(app.editor).apply()
profile.serialize(app.dataStore)
case None => getActivity.finish()
}
addPreferencesFromResource(R.xml.pref_profile)
......@@ -78,7 +78,8 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
plugin.setOnPreferenceChangeListener((_, value) => {
val selected = value.asInstanceOf[String]
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.setText(pluginConfiguration.selectedOptions.toString)
if (!PluginManager.fetchPlugins()(selected).trusted)
......@@ -88,7 +89,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
pluginConfigure.setOnPreferenceChangeListener(onPluginConfigureChanged)
initPlugins()
app.listenForPackageChanges(initPlugins())
app.settings.registerOnSharedPreferenceChangeListener(this)
app.dataStore.registerChangeListener(this)
}
def initPlugins() {
......@@ -97,7 +98,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
plugin.setEntryValues(plugins.map(_._2.id.asInstanceOf[CharSequence]).toArray)
plugin.setEntryIcons(plugins.map(_._2.icon).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.init()
plugin.checkSummary()
......@@ -109,7 +110,8 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
val selected = pluginConfiguration.selected
pluginConfiguration = new PluginConfiguration(pluginConfiguration.pluginsOptions +
(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
} catch {
case exc: IllegalArgumentException =>
......@@ -117,17 +119,17 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
false
}
override def onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String): Unit =
if (key != Key.proxyApps && findPreference(key) != null) app.editor.putBoolean(Key.dirty, true).apply()
def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String): Unit =
if (key != Key.proxyApps && findPreference(key) != null) app.dataStore.dirty = true
override def onDestroy() {
app.settings.unregisterOnSharedPreferenceChangeListener(this)
app.dataStore.unregisterChangeListener(this)
super.onDestroy()
}
override def 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() {
......@@ -176,7 +178,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
}
def saveAndExit() {
profile.deserialize(app.settings)
profile.deserialize(app.dataStore)
app.profileManager.updateProfile(profile)
if (ProfilesFragment.instance != null) ProfilesFragment.instance.profilesAdapter.deepRefreshId(profile.id)
getActivity.finish()
......
......@@ -68,7 +68,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
case _ => false
}
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 _ => false
}
......@@ -78,20 +78,20 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
var item: Profile = _
private val text1 = itemView.findViewById(android.R.id.text1).asInstanceOf[TextView]
private val text2 = itemView.findViewById(android.R.id.text2).asInstanceOf[TextView]
private val traffic = itemView.findViewById(R.id.traffic).asInstanceOf[TextView]
private val edit = itemView.findViewById(R.id.edit)
private val text1 = itemView.findViewById[TextView](android.R.id.text1)
private val text2 = itemView.findViewById[TextView](android.R.id.text2)
private val traffic = itemView.findViewById[TextView](R.id.traffic)
private val edit = itemView.findViewById[View](R.id.edit)
edit.setOnClickListener(_ => startConfig(item.id))
edit.setOnLongClickListener(cardButtonLongClickListener)
itemView.setOnClickListener(this)
// 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 = _
{
val share = itemView.findViewById(R.id.share)
val share = itemView.findViewById[View](R.id.share)
share.setOnClickListener(_ => {
val popup = new PopupMenu(getActivity, share)
popup.getMenuInflater.inflate(R.menu.profile_share_popup, popup.getMenu)
......@@ -129,7 +129,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
TrafficMonitor.formatTraffic(tx), TrafficMonitor.formatTraffic(rx)))
}
if (item.id == app.profileId) {
if (item.id == app.dataStore.profileId) {
itemView.setSelected(true)
selectedItem = this
} else {
......@@ -147,7 +147,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
adView.setLayoutParams(params)
adView.setAdUnitId("ca-app-pub-9097031975646651/5224027521")
adView.setAdSize(new AdSize(328, 132))
itemView.findViewById(R.id.content).asInstanceOf[LinearLayout].addView(adView)
itemView.findViewById[LinearLayout](R.id.content).addView(adView)
// Demographics
val random = new Random()
......@@ -166,11 +166,11 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
def onClick(v: View): Unit = if (isEnabled) {
val activity = getActivity.asInstanceOf[MainActivity]
val old = app.profileId
val old = app.dataStore.profileId
app.switchProfile(item.id)
profilesAdapter.refreshId(old)
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 {
......@@ -251,7 +251,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
if (index >= 0) {
profiles.remove(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
toolbar.inflateMenu(R.menu.profile_manager_menu)
toolbar.setOnMenuItemClickListener(this)
if (app.profileManager.getFirstProfile.isEmpty) app.profileId(app.profileManager.createProfile().id)
val profilesList = view.findViewById(R.id.list).asInstanceOf[RecyclerView]
if (app.profileManager.getFirstProfile.isEmpty) app.dataStore.profileId = app.profileManager.createProfile().id
val profilesList = view.findViewById[RecyclerView](R.id.list)
val layoutManager = new LinearLayoutManager(getActivity, LinearLayoutManager.VERTICAL, false)
profilesList.setLayoutManager(layoutManager)
layoutManager.scrollToPosition(profilesAdapter.profiles.zipWithIndex.collectFirst {
case (profile, i) if profile.id == app.profileId => i
case (profile, i) if profile.id == app.dataStore.profileId => i
}.getOrElse(-1))
val animator = new DefaultItemAnimator()
animator.setSupportsChangeAnimations(false) // prevent fading-in/out when rebinding
......
......@@ -24,6 +24,8 @@ import android.app.Activity
import android.content.Intent
import android.content.pm.ShortcutManager
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}
/**
......@@ -34,11 +36,12 @@ class QuickToggleShortcut extends Activity with ServiceBoundContext {
super.onCreate(savedInstanceState)
getIntent.getAction match {
case Intent.ACTION_CREATE_SHORTCUT =>
setResult(Activity.RESULT_OK, new Intent()
.putExtra(Intent.EXTRA_SHORTCUT_INTENT, new Intent(this, classOf[QuickToggleShortcut]))
.putExtra(Intent.EXTRA_SHORTCUT_NAME, getString(R.string.quick_toggle))
.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
Intent.ShortcutIconResource.fromContext(this, R.mipmap.ic_launcher)))
setResult(Activity.RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this,
new ShortcutInfoCompat.Builder(this, "toggle")
.setIntent(new Intent(this, classOf[QuickToggleShortcut]).setAction(Intent.ACTION_MAIN))
.setIcon(IconCompat.createWithResource(this, R.drawable.ic_qu_shadowsocks_launcher))
.setShortLabel(getString(R.string.quick_toggle))
.build()))
finish()
case _ =>
attachService()
......
......@@ -24,16 +24,16 @@ import java.io.{File, FileOutputStream, IOException}
import java.util.Locale
import android.annotation.SuppressLint
import android.app.Application
import android.app.{Application, NotificationChannel, NotificationManager}
import android.content._
import android.content.res.Configuration
import android.os.{Build, LocaleList}
import android.preference.PreferenceManager
import android.support.v7.app.AppCompatDelegate
import android.util.Log
import com.evernote.android.job.JobManager
import com.github.shadowsocks.acl.DonaldTrump
import com.github.shadowsocks.database.{DBHelper, Profile, ProfileManager}
import com.github.shadowsocks.preference.OrmLitePreferenceDataStore
import com.github.shadowsocks.utils.CloseUtils._
import com.github.shadowsocks.utils._
import com.google.android.gms.analytics.{GoogleAnalytics, HitBuilders, StandardExceptionParser, Tracker}
......@@ -42,6 +42,7 @@ import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.j256.ormlite.logger.LocalLog
import eu.chainfire.libsuperuser.Shell
import scala.collection.JavaConversions._
import scala.collection.mutable.ArrayBuffer
object ShadowsocksApplication {
......@@ -59,13 +60,13 @@ object ShadowsocksApplication {
class ShadowsocksApplication extends Application {
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 settings: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
lazy val editor: SharedPreferences.Editor = settings.edit
lazy val profileManager = new ProfileManager(new DBHelper(this))
private lazy val dbHelper = new DBHelper(this)
lazy val profileManager = new ProfileManager(dbHelper)
lazy val dataStore = new OrmLitePreferenceDataStore(dbHelper)
def isNatEnabled: Boolean = settings.getBoolean(Key.isNAT, false)
def isNatEnabled: Boolean = dataStore.isNAT
def isVpnEnabled: Boolean = !isNatEnabled
// send event
......@@ -78,13 +79,11 @@ class ShadowsocksApplication extends Application {
.setFatal(false)
.build())
def profileId: Int = settings.getInt(Key.id, 0)
def profileId(i: Int): Unit = editor.putInt(Key.id, i).apply()
def currentProfile: Option[Profile] = profileManager.getProfile(profileId)
def currentProfile: Option[Profile] = profileManager.getProfile(dataStore.profileId)
def switchProfile(id: Int): Profile = {
val result = profileManager.getProfile(id) getOrElse profileManager.createProfile()
profileId(result.id)
dataStore.profileId = result.id
result
}
......@@ -153,7 +152,12 @@ class ShadowsocksApplication extends Application {
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() {
......@@ -186,10 +190,10 @@ class ShadowsocksApplication extends Application {
autoClose(new FileOutputStream(new File(getFilesDir, file)))(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 = {
val filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED)
......
......@@ -20,26 +20,11 @@
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.database.DBHelper
class ShadowsocksBackupAgent extends BackupAgentHelper {
// The names of the SharedPreferences groups that the application maintains. These
// 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"))
}
override def onCreate(): Unit = addHelper("com.github.shadowsocks.database.profile",
new FileBackupHelper(this, "../databases/" + DBHelper.PROFILE, Acl.CUSTOM_RULES + ".acl"))
}
......@@ -24,11 +24,12 @@ import java.io.File
import java.net.{Inet6Address, InetAddress}
import java.util.Locale
import android.app.Service
import android.content._
import android.os._
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.acl.{AclSyncJob, Acl}
import com.github.shadowsocks.acl.{Acl, AclSyncJob}
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils._
import eu.chainfire.libsuperuser.Shell
......@@ -43,7 +44,6 @@ class ShadowsocksNatService extends BaseService {
val CMD_IPTABLES_DNAT_ADD_SOCKS =
"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()
var sslocalProcess: GuardedProcess = _
......@@ -177,15 +177,17 @@ class ShadowsocksNatService extends BaseService {
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, _) =>
if (exitCode == 0) super.startRunner(profile) else {
if (exitCode == 0) super.onStartCommand(intent, flags, startId) else {
if (su != null) {
su.close()
su = null
}
super.stopRunner(stopService = true, getString(R.string.nat_no_root))
})
Service.START_NOT_STICKY
} else super.onStartCommand(intent, flags, startId)
override def connect() {
super.connect()
......@@ -204,12 +206,11 @@ class ShadowsocksNatService extends BaseService {
AclSyncJob.schedule(profile.route)
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
changeState(State.STOPPING)
......
......@@ -28,13 +28,15 @@ import android.os.{Build, PowerManager}
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationCompat.BigTextStyle
import android.support.v4.content.ContextCompat
import android.support.v4.os.BuildCompat
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback.Stub
import com.github.shadowsocks.utils.{Action, State, TrafficMonitor, Utils}
/**
* @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 lazy val nm = service.getSystemService(Context.NOTIFICATION_SERVICE).asInstanceOf[NotificationManager]
private lazy val callback = new Stub {
......@@ -52,7 +54,7 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
private var lockReceiver: BroadcastReceiver = _
private var callbackRegistered: Boolean = _
private val builder = new NotificationCompat.Builder(service)
private val builder = new NotificationCompat.Builder(service, channel)
.setWhen(0)
.setColor(ContextCompat.getColor(service, R.color.material_primary_500))
.setTicker(service.getString(R.string.forward_success))
......
......@@ -26,6 +26,7 @@ import android.net.VpnService
import android.os.{Bundle, Handler}
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.Utils
object ShadowsocksRunnerActivity {
private final val TAG = "ShadowsocksRunnerActivity"
......@@ -46,7 +47,7 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
def startBackgroundService() {
if (app.isNatEnabled) {
bgService.use(app.profileId)
Utils.startSsService(this)
finish()
} else {
val intent = VpnService.prepare(ShadowsocksRunnerActivity.this)
......@@ -73,7 +74,6 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
} else {
attachService()
}
finish()
}
override def onDestroy() {
......@@ -87,10 +87,7 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
resultCode match {
case Activity.RESULT_OK =>
if (bgService != null) {
bgService.use(app.profileId)
}
case Activity.RESULT_OK => Utils.startSsService(this)
case _ =>
Log.e(TAG, "Failed to start VpnService")
}
......
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import android.app.Service
import android.content.Intent
import android.net.VpnService
import android.os.{Handler, IBinder}
import com.github.shadowsocks.ShadowsocksApplication.app
class ShadowsocksRunnerService extends Service with ServiceBoundContext {
val handler = new Handler()
override def onBind(intent: Intent): IBinder = {
null
}
override def onServiceConnected() {
handler.postDelayed(() => {
if (bgService != null) {
if (app.isNatEnabled) startBackgroundService()
else if (VpnService.prepare(ShadowsocksRunnerService.this) == null) startBackgroundService()
handler.postDelayed(() => stopSelf(), 3000)
}
}, 1000)
}
def startBackgroundService(): Unit = bgService.useSync(app.profileId)
override def onCreate() {
super.onCreate()
attachService()
}
override def onDestroy() {
super.onDestroy()
detachService()
}
}
......@@ -23,6 +23,7 @@ package com.github.shadowsocks
import java.io.File
import java.util.Locale
import android.app.Service
import android.content._
import android.content.pm.PackageManager.NameNotFoundException
import android.net.VpnService
......@@ -30,7 +31,6 @@ import android.os._
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.acl.{Acl, AclSyncJob, Subnet}
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils._
import scala.collection.mutable.ArrayBuffer
......@@ -42,7 +42,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
val PRIVATE_VLAN6 = "fdfe:dcba:9876::%s"
var conn: ParcelFileDescriptor = _
var vpnThread: ShadowsocksVpnThread = _
private var notification: ShadowsocksNotification = _
var sslocalProcess: GuardedProcess = _
var overtureProcess: GuardedProcess = _
......@@ -69,8 +68,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
vpnThread = null
}
if (notification != null) notification.destroy()
// channge the state
changeState(State.STOPPING)
......@@ -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
if (VpnService.prepare(this) != null) {
val i = new Intent(this, classOf[ShadowsocksRunnerActivity])
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(i)
stopRunner(stopService = true)
return
}
super.startRunner(profile)
Service.START_NOT_STICKY
} else super.onStartCommand(intent, flags, startId)
}
override def createNotification() = new ShadowsocksNotification(this, profile.name, "service-vpn")
override def connect() {
super.connect()
......@@ -137,8 +133,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
if (profile.route != Acl.ALL && profile.route != Acl.CUSTOM_RULES)
AclSyncJob.schedule(profile.route)
notification = new ShadowsocksNotification(this, profile.name)
}
/** Called when the activity is first created. */
......
......@@ -42,7 +42,7 @@ class TaskerActivity extends AppCompatActivity {
typedArray.recycle()
}
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)
def bindDefault() {
......
......@@ -30,10 +30,17 @@ import com.github.shadowsocks.ShadowsocksApplication.app
class TaskerReceiver extends BroadcastReceiver {
override def onReceive(context: Context, intent: Intent) {
val settings = TaskerSettings.fromIntent(intent)
var changed = false
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 _ =>
}
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 {
override def onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
toolbar = view.findViewById(R.id.toolbar).asInstanceOf[Toolbar]
toolbar = view.findViewById[Toolbar](R.id.toolbar)
val activity = getActivity.asInstanceOf[MainActivity]
if (activity.crossfader == null) activity.drawer.setToolbar(activity, toolbar, true)
}
......
......@@ -26,26 +26,6 @@ class Acl {
@DatabaseField
var bypass: Boolean = _
def getBypassHostnamesString: String = bypassHostnames.mkString("\n")
def getProxyHostnamesString: String = proxyHostnames.mkString("\n")
def getSubnetsString: String = subnets.mkString("\n")
def setBypassHostnamesString(value: String) {
bypassHostnames.clear()
bypassHostnames ++= value.split("\n")
}
def setProxyHostnamesString(value: String) {
proxyHostnames.clear()
proxyHostnames ++= value.split("\n")
}
def setSubnetsString(value: String) {
subnets.clear()
subnets ++= value.split("\n").map(Subnet.fromString)
}
def setUrlRules(value: String) {
urls.clear()
urls ++= value.split("\n")
}
def fromAcl(other: Acl): Acl = {
bypassHostnames.clear()
bypassHostnames ++= other.bypassHostnames
......@@ -71,17 +51,10 @@ class Acl {
var in_urls = false
for (line <- value.getLines()) (line.indexOf('#') match {
case -1 => if (!in_urls) line else ""
case index => {
line.indexOf("URLS_BEGIN") match {
case -1 =>
case index => in_urls = true
}
line.indexOf("URLS_END") match {
case -1 =>
case index => in_urls = false
}
"" // ignore any comment lines
}
case index =>
if (line.contains("URLS_BEGIN")) in_urls = true
if (line.contains("URLS_END")) in_urls = false
line.substring(0, index) // trim comments
}).trim match {
case "[outbound_block_list]" =>
hostnames = null
......@@ -95,10 +68,8 @@ class Acl {
case "[reject_all]" | "[bypass_all]" => bypass = true
case "[accept_all]" | "[proxy_all]" => bypass = false
case input if subnets != null && input.nonEmpty => try subnets += Subnet.fromString(input) catch {
case _: IllegalArgumentException => if (input.startsWith("http://") || input.startsWith("https://")) {
urls += input
}
hostnames += input
case _: IllegalArgumentException => if (input.startsWith("http://") || input.startsWith("https://"))
urls += input else hostnames += input
}
case _ =>
}
......@@ -116,7 +87,7 @@ class Acl {
try {
urls.foreach((url: String) => result.append(Source.fromURL(url).mkString))
} catch {
case e: IOException => // ignore
case _: IOException => // ignore
}
}
result.append("#URLS_END\n")
......@@ -141,9 +112,7 @@ class Acl {
result.toString
}
override def toString: String = {
getAclString(false)
}
override def toString: String = getAclString(false)
def isValidCustomRules: Boolean = bypass && bypassHostnames.isEmpty
......
......@@ -46,8 +46,8 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
private def createAclRuleDialog(text: CharSequence = "") = {
val view = getActivity.getLayoutInflater.inflate(R.layout.dialog_acl_rule, null)
val templateSelector = view.findViewById(R.id.template_selector).asInstanceOf[Spinner]
val editText = view.findViewById(R.id.content).asInstanceOf[EditText]
val templateSelector = view.findViewById[Spinner](R.id.template_selector)
val editText = view.findViewById[EditText](R.id.content)
PATTERN_DOMAIN.findFirstMatchIn(text) match {
case Some(m) =>
templateSelector.setSelection(DOMAIN)
......@@ -70,7 +70,7 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
private final class AclRuleViewHolder(view: View) extends RecyclerView.ViewHolder(view)
with View.OnClickListener with View.OnLongClickListener {
var item: AnyRef = _
private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[TextView]
private val text = itemView.findViewById[TextView](android.R.id.text1)
itemView.setOnClickListener(this)
itemView.setOnLongClickListener(this)
itemView.setBackgroundResource(R.drawable.background_selectable)
......@@ -252,12 +252,11 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
toolbar.setOnMenuItemClickListener(this)
selectionItem = toolbar.getMenu.findItem(R.id.selection)
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.setItemAnimator(new DefaultItemAnimator)
list.setAdapter(adapter)
val fastScroller = view.findViewById(R.id.fastscroller).asInstanceOf[FastScroller]
fastScroller.setRecyclerView(list)
view.findViewById[FastScroller](R.id.fastscroller).setRecyclerView(list)
undoManager = new UndoSnackbarManager[AnyRef](getActivity.findViewById(R.id.snackbar), adapter.undo)
new ItemTouchHelper(new SimpleCallback(0, ItemTouchHelper.START | ItemTouchHelper.END) {
override def getSwipeDirs(recyclerView: RecyclerView, viewHolder: ViewHolder): Int =
......
......@@ -20,10 +20,14 @@
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.database.sqlite.SQLiteDatabase
import android.preference.PreferenceManager
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.Key
import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper
import com.j256.ormlite.dao.Dao
import com.j256.ormlite.support.ConnectionSource
......@@ -46,17 +50,20 @@ object DBHelper {
}
class DBHelper(val context: Context)
extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 22) {
extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 23) {
import DBHelper._
lazy val profileDao: Dao[Profile, Int] = getDao(classOf[Profile])
lazy val kvPairDao: Dao[KeyValuePair, String] = getDao(classOf[KeyValuePair])
def onCreate(database: SQLiteDatabase, connectionSource: ConnectionSource) {
TableUtils.createTable(connectionSource, classOf[Profile])
TableUtils.createTable(connectionSource, classOf[KeyValuePair])
}
def recreate(database: SQLiteDatabase, connectionSource: ConnectionSource) {
TableUtils.dropTable(connectionSource, classOf[Profile], true)
TableUtils.dropTable(connectionSource, classOf[KeyValuePair], true)
onCreate(database, connectionSource)
}
......@@ -132,6 +139,19 @@ class DBHelper(val context: Context)
profileDao.executeRawNoArgs("DROP TABLE `tmp`;")
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 {
case ex: Exception =>
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
import java.util.Locale
import android.content.SharedPreferences
import android.net.Uri
import android.os.Binder
import android.util.Base64
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.preference.OrmLitePreferenceDataStore
import com.github.shadowsocks.utils.Key
import com.j256.ormlite.field.{DataType, DatabaseField}
......@@ -108,37 +108,38 @@ class Profile {
}
override def toString: String = toUri.toString
def serialize(editor: SharedPreferences.Editor): SharedPreferences.Editor = editor
.putString(Key.name, name)
.putString(Key.host, host)
.putInt(Key.localPort, localPort)
.putInt(Key.remotePort, remotePort)
.putString(Key.password, password)
.putString(Key.route, route)
.putString(Key.remoteDns, remoteDns)
.putString(Key.method, method)
.putBoolean(Key.proxyApps, proxyApps)
.putBoolean(Key.bypass, bypass)
.putBoolean(Key.udpdns, udpdns)
.putBoolean(Key.ipv6, ipv6)
.putString(Key.individual, individual)
.putString(Key.plugin, plugin)
.remove(Key.dirty)
def deserialize(pref: SharedPreferences) {
def serialize(store: OrmLitePreferenceDataStore) {
store.putString(Key.name, name)
store.putString(Key.host, host)
store.putInt(Key.localPort, localPort)
store.putInt(Key.remotePort, remotePort)
store.putString(Key.password, password)
store.putString(Key.route, route)
store.putString(Key.remoteDns, remoteDns)
store.putString(Key.method, method)
store.proxyApps = proxyApps
store.bypass = bypass
store.putBoolean(Key.udpdns, udpdns)
store.putBoolean(Key.ipv6, ipv6)
store.individual = individual
store.plugin = plugin
store.remove(Key.dirty)
}
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
name = pref.getString(Key.name, null)
host = pref.getString(Key.host, null)
localPort = pref.getInt(Key.localPort, 0)
remotePort = pref.getInt(Key.remotePort, 0)
password = pref.getString(Key.password, null)
method = pref.getString(Key.method, null)
route = pref.getString(Key.route, null)
remoteDns = pref.getString(Key.remoteDns, null)
proxyApps = pref.getBoolean(Key.proxyApps, false)
bypass = pref.getBoolean(Key.bypass, false)
udpdns = pref.getBoolean(Key.udpdns, false)
ipv6 = pref.getBoolean(Key.ipv6, false)
individual = pref.getString(Key.individual, null)
plugin = pref.getString(Key.plugin, null)
name = store.getString(Key.name, null)
host = store.getString(Key.host, null)
localPort = store.getInt(Key.localPort, 0)
remotePort = store.getInt(Key.remotePort, 0)
password = store.getString(Key.password, null)
method = store.getString(Key.method, null)
route = store.getString(Key.route, null)
remoteDns = store.getString(Key.remoteDns, null)
proxyApps = store.proxyApps
bypass = store.bypass
udpdns = store.getBoolean(Key.udpdns, false)
ipv6 = store.getBoolean(Key.ipv6, false)
individual = store.individual
plugin = store.plugin
}
}
......@@ -46,9 +46,9 @@ final class BottomSheetPreferenceDialogFragment extends PreferenceDialogFragment
private final class IconListViewHolder(val dialog: BottomSheetDialog, view: View) extends ViewHolder(view)
with View.OnClickListener with View.OnLongClickListener {
private var index: Int = _
private val text1 = view.findViewById(android.R.id.text1).asInstanceOf[TextView]
private val text2 = view.findViewById(android.R.id.text2).asInstanceOf[TextView]
private val icon = view.findViewById(android.R.id.icon).asInstanceOf[ImageView]
private val text1 = view.findViewById[TextView](android.R.id.text1)
private val text2 = view.findViewById[TextView](android.R.id.text2)
private val icon = view.findViewById[ImageView](android.R.id.icon)
view.setOnClickListener(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 {
object Action {
final val SERVICE = "com.github.shadowsocks.SERVICE"
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"
}
......@@ -33,7 +33,7 @@ import android.view.View.MeasureSpec
import android.view.{Gravity, View, Window}
import android.widget.Toast
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.{BuildConfig, ShadowsocksRunnerService}
import com.github.shadowsocks.{BuildConfig, ShadowsocksNatService, ShadowsocksVpnService}
import org.xbill.DNS._
import scala.collection.JavaConversions._
......@@ -137,10 +137,11 @@ object Utils {
}
def startSsService(context: Context) {
val intent = new Intent(context, classOf[ShadowsocksRunnerService])
context.startService(intent)
val 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) {
val intent = new Intent(Action.CLOSE)
context.sendBroadcast(intent)
......
* 0.0.4:
* Enlarge text size of number pickers;
* Update support library version to 26.0.0.
* 0.0.3:
* Update support library version to 25.2.0.
* 0.0.2:
......
......@@ -36,7 +36,7 @@ First you need to add this library to your dependencies. This library is written
and it's most convenient to use it with SBT:
```scala
libraryDependencies += "com.github.shadowsocks" %% "plugin" % "0.0.2"
libraryDependencies += "com.github.shadowsocks" %% "plugin" % "0.0.4"
```
### Native binary configuration
......
......@@ -2,7 +2,7 @@ enablePlugins(AndroidLib)
android.useSupportVectors
name := "plugin"
version := "0.0.3"
version := "0.0.4"
pomExtra in Global := {
<url>https://github.com/shadowsocks/shadowsocks-android</url>
......
......@@ -2,10 +2,10 @@
package="com.github.shadowsocks.plugin">
<uses-sdk
android:minSdkVersion="19"
android:targetSdkVersion="25"/>
android:targetSdkVersion="26"/>
<application
android:theme="@style/Theme.Shadowsocks">
<meta-data android:name="com.github.shadowsocks.plugin.version"
android:value="0.0.2"/>
android:value="0.0.4"/>
</application>
</manifest>
......@@ -40,6 +40,6 @@
<item name="android:scrollbars">vertical</item>
</style>
<style name="NumberPickerStyle">
<item name="android:textSize">14sp</item>
<item name="android:textSize">18sp</item>
</style>
</resources>
......@@ -60,7 +60,8 @@ class EditTextPreference(context: Context, attrs: AttributeSet = null) extends P
}
override def setText(text: String): Unit = {
val old = getText
super.setText(text)
notifyChanged()
if (old != text) notifyChanged()
}
}
......@@ -36,7 +36,7 @@ class EditTextPreferenceDialogFragment extends PreferenceDialogFragment {
val oldParent = editText.getParent.asInstanceOf[ViewGroup]
if (oldParent eq view) return
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
val container = oldEdit.getParent.asInstanceOf[ViewGroup]
if (container == null) return
......
......@@ -77,7 +77,7 @@ class PreferenceGroupAdapter(group: PreferenceGroup) extends Old(group) {
view.setBackground(background)
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) {
val widgetResId = fieldWidgetResId.get(pl).asInstanceOf[Int]
if (widgetResId != 0) inflater.inflate(widgetResId, widgetFrame) else widgetFrame.setVisibility(View.GONE)
......
addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.7")
addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.8")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.10")
......
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