From 143afb052de0b357db02d199c1f2f7d4d298d6f8 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sat, 15 Jul 2017 01:42:17 +0800 Subject: [PATCH] Use startService calls instead of AIDL to start services Because our data store is process-safe, we can remove the `use` function entirely and move more components to :bg process. --- mobile/src/main/AndroidManifest.xml | 12 +- .../shadowsocks/aidl/IShadowsocksService.aidl | 3 - .../com/github/shadowsocks/BaseService.scala | 103 +++++++++--------- .../com/github/shadowsocks/MainActivity.scala | 8 +- .../github/shadowsocks/ProfilesFragment.scala | 2 +- .../shadowsocks/ShadowsocksNatService.scala | 9 +- .../ShadowsocksRunnerActivity.scala | 8 +- .../ShadowsocksRunnerService.scala | 65 ----------- .../shadowsocks/ShadowsocksVpnService.scala | 11 +- .../github/shadowsocks/TaskerReceiver.scala | 11 +- .../github/shadowsocks/utils/Constants.scala | 1 + .../com/github/shadowsocks/utils/Utils.scala | 10 +- 12 files changed, 90 insertions(+), 153 deletions(-) delete mode 100644 mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 8086c1e9..f4d102ed 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -96,18 +96,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"> - - - - + - + diff --git a/mobile/src/main/aidl/com/github/shadowsocks/aidl/IShadowsocksService.aidl b/mobile/src/main/aidl/com/github/shadowsocks/aidl/IShadowsocksService.aidl index 17545289..5735de28 100644 --- a/mobile/src/main/aidl/com/github/shadowsocks/aidl/IShadowsocksService.aidl +++ b/mobile/src/main/aidl/com/github/shadowsocks/aidl/IShadowsocksService.aidl @@ -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); } diff --git a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala index 3c9835e1..83765492 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala @@ -27,7 +27,7 @@ import java.util.concurrent.TimeUnit import java.util.{Timer, TimerTask} import android.app.Service -import android.content.{BroadcastReceiver, Context, Intent, IntentFilter} +import android.content._ import android.os.{Build, Handler, IBinder, RemoteCallbackList} import android.text.TextUtils import android.util.Log @@ -65,9 +65,11 @@ trait BaseService extends Service { lazy val restartHanlder = new Handler(getMainLooper) private var notification: ShadowsocksNotification = _ - private val closeReceiver: BroadcastReceiver = (context: Context, _: Intent) => { - Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show() - stopRunner(stopService = true) + 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,6 +112,17 @@ trait BaseService extends Service { false } 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() { profile.name = profile.getName // save original name before it's (possibly) overwritten by IP addresses @@ -161,38 +160,8 @@ trait BaseService extends Service { } def createNotification(): ShadowsocksNotification - def startRunner(profile: Profile) { - this.profile = profile - - if (Build.VERSION.SDK_INT >= 26) startForegroundService(new Intent(this, getClass)) + def startRunner(): Unit = if (Build.VERSION.SDK_INT >= 26) startForegroundService(new Intent(this, getClass)) else 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 - } - - 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) - }) - } def stopRunner(stopService: Boolean, msg: String = null) { // clean up recevier @@ -281,7 +250,41 @@ 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 + 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) diff --git a/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala b/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala index 4908bf47..b3768b3b 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala @@ -175,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.dataStore.profileId) + case Activity.RESULT_OK => Utils.startSsService(this) case _ => Log.e(TAG, "Failed to start VpnService") } @@ -299,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.dataStore.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)) @@ -369,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() diff --git a/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala index 2fd177c5..618a7931 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala @@ -170,7 +170,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic 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 { diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala index a71ea95d..a1db6d74 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala @@ -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 @@ -176,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() diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala index 197cc935..749560be 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala @@ -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.dataStore.profileId) + Utils.startSsService(this) finish() } else { val intent = VpnService.prepare(ShadowsocksRunnerActivity.this) @@ -87,10 +88,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.dataStore.profileId) - } + case Activity.RESULT_OK => Utils.startSsService(this) case _ => Log.e(TAG, "Failed to start VpnService") } diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala deleted file mode 100644 index e9079c46..00000000 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala +++ /dev/null @@ -1,65 +0,0 @@ -/*******************************************************************************/ -/* */ -/* Copyright (C) 2017 by Max Lv */ -/* Copyright (C) 2017 by Mygod Studio */ -/* */ -/* 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 . */ -/* */ -/*******************************************************************************/ - -package com.github.shadowsocks - -import android.app.Service -import android.content.Intent -import android.net.VpnService -import android.os.{Build, Handler, IBinder} -import android.support.v4.app.NotificationCompat -import android.support.v4.os.BuildCompat -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.dataStore.profileId) - - override def onCreate() { - super.onCreate() - if (Build.VERSION.SDK_INT >= 26) { - val builder = new NotificationCompat.Builder(this) - builder.setPriority(NotificationCompat.PRIORITY_MIN) - startForeground(1, builder.build) - } - attachService() - } - - override def onDestroy() { - super.onDestroy() - stopForeground(true) - detachService() - } -} diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala index 5e4d1e34..1f124776 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala @@ -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 @@ -100,18 +100,15 @@ 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") diff --git a/mobile/src/main/scala/com/github/shadowsocks/TaskerReceiver.scala b/mobile/src/main/scala/com/github/shadowsocks/TaskerReceiver.scala index b015eb65..fa9017b8 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/TaskerReceiver.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/TaskerReceiver.scala @@ -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) } } diff --git a/mobile/src/main/scala/com/github/shadowsocks/utils/Constants.scala b/mobile/src/main/scala/com/github/shadowsocks/utils/Constants.scala index 80bd0f4a..e7e30a92 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/utils/Constants.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/utils/Constants.scala @@ -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" } diff --git a/mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala b/mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala index 5bda5098..eae40a19 100644 --- a/mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala +++ b/mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala @@ -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,11 +137,11 @@ object Utils { } def startSsService(context: Context) { - val intent = new Intent(context, classOf[ShadowsocksRunnerService]) - if (Build.VERSION.SDK_INT >= 26) context.startForegroundService(intent) - else 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)