diff --git a/android/app/libs/liblantern-all.aar b/android/app/libs/liblantern-all.aar index fd4be06b0..4961af106 100644 --- a/android/app/libs/liblantern-all.aar +++ b/android/app/libs/liblantern-all.aar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:091cd2629b6eff2c058dcdf883fb75fa200f00a7cf768e6eae0b2d6b064f926f -size 77696936 +oid sha256:9edf75981e16250fc6397ec39f5a7d9bd6487961d0951ad8e016b2f078ddab2c +size 77698340 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d986dd656..bba15cdc1 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -64,20 +64,12 @@ + android:process=":bg" + android:name=".service.LanternService"> - - - - - - @@ -117,29 +109,13 @@ + android:permission="android.permission.BIND_VPN_SERVICE" + android:process=":bg"> - - - - - - - - - - - - - - - diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/Constants.kt b/android/app/src/main/kotlin/org/getlantern/lantern/Constants.kt new file mode 100644 index 000000000..ef73cd49b --- /dev/null +++ b/android/app/src/main/kotlin/org/getlantern/lantern/Constants.kt @@ -0,0 +1,9 @@ +package org.getlantern.lantern + +object Actions { + const val CONNECT_VPN = "org.getlantern.lantern.intent.VPN_CONNECTED" + const val DISCONNECT_VPN = "org.getlantern.lantern.intent.VPN_DISCONNECTED" + + const val STOP_SERVICE = "org.getlantern.lantern.service.STOP" + const val SERVICE = "org.getlantern.lantern.service" +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/LanternApp.kt b/android/app/src/main/kotlin/org/getlantern/lantern/LanternApp.kt index 36a8047cc..9456d565f 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/LanternApp.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/LanternApp.kt @@ -1,13 +1,18 @@ package org.getlantern.lantern import android.app.Application +import android.app.NotificationManager import android.content.Context +import android.content.Intent import android.os.StrictMode import androidx.appcompat.app.AppCompatDelegate +import androidx.core.content.ContextCompat import androidx.multidex.MultiDex import org.getlantern.lantern.model.InAppBilling import org.getlantern.lantern.model.LanternHttpClient import org.getlantern.lantern.model.LanternSessionManager +import org.getlantern.lantern.notification.Notifications +import org.getlantern.lantern.service.LanternConnection import org.getlantern.lantern.util.debugOnly import org.getlantern.lantern.util.LanternProxySelector import org.getlantern.lantern.util.SentryUtil @@ -60,6 +65,7 @@ open class LanternApp : Application() { override fun attachBaseContext(base: Context) { super.attachBaseContext(base) + application = this // this is necessary running earlier versions of Android // multidex support has to be added manually // in addition to being enabled in the app build.gradle @@ -69,11 +75,28 @@ open class LanternApp : Application() { companion object { private val TAG = LanternApp::class.java.simpleName + + lateinit var application: LanternApp + private lateinit var appContext: Context private lateinit var inAppBilling: InAppBilling private lateinit var lanternHttpClient: LanternHttpClient private lateinit var session: LanternSessionManager + val notificationManager by lazy { application. + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } + + fun startService(connection: LanternConnection) = ContextCompat.startForegroundService( + application, Intent(application, connection.serviceClass).apply { + action = if (connection.isVpnService) Actions.CONNECT_VPN else null + } + ) + + fun stopService(connection: LanternConnection) = + application.sendBroadcast(Intent(Actions.STOP_SERVICE). + setPackage(application.packageName)) + + @JvmStatic fun getAppContext(): Context { return appContext diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt b/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt index 62fd3d72f..d35247fb1 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt @@ -3,7 +3,6 @@ package org.getlantern.lantern import android.annotation.SuppressLint import android.content.Context import android.content.Intent -import android.content.IntentFilter import android.content.pm.PackageManager import android.net.VpnService import android.os.Bundle @@ -18,7 +17,6 @@ import androidx.core.content.ContextCompat import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngineCache -import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.lantern.model.MessagingModel import io.lantern.model.ReplicaModel @@ -29,6 +27,7 @@ import kotlinx.coroutines.* import okhttp3.Response import org.getlantern.lantern.activity.WebViewActivity_ import org.getlantern.lantern.event.EventManager +import org.getlantern.lantern.loconf.SurveyHelper import org.getlantern.lantern.model.AccountInitializationStatus import org.getlantern.lantern.model.Bandwidth import org.getlantern.lantern.model.LanternHttpClient.PlansCallback @@ -42,30 +41,26 @@ import org.getlantern.lantern.model.ProUser import org.getlantern.lantern.model.Stats import org.getlantern.lantern.model.Utils import org.getlantern.lantern.model.VpnState -import org.getlantern.lantern.notification.NotificationHelper -import org.getlantern.lantern.notification.NotificationReceiver +import org.getlantern.lantern.service.LanternConnection import org.getlantern.lantern.plausible.Plausible -import org.getlantern.lantern.service.LanternService_ import org.getlantern.lantern.util.PermissionUtil import org.getlantern.lantern.util.PlansUtil +import org.getlantern.lantern.util.isServiceRunning import org.getlantern.lantern.util.restartApp import org.getlantern.lantern.util.showAlertDialog import org.getlantern.lantern.vpn.LanternVpnService +import org.getlantern.lantern.vpn.VpnServiceManager import org.getlantern.mobilesdk.Logger import org.getlantern.mobilesdk.model.Event import org.getlantern.mobilesdk.model.LoConf import org.getlantern.mobilesdk.model.LoConf.Companion.fetch -import org.getlantern.mobilesdk.model.Survey import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import java.util.Locale import java.util.concurrent.* -class MainActivity : - FlutterActivity(), - MethodChannel.MethodCallHandler, - CoroutineScope by MainScope() { +class MainActivity : FlutterActivity(), CoroutineScope by MainScope() { private lateinit var messagingModel: MessagingModel private lateinit var vpnModel: VpnModel private lateinit var sessionModel: SessionModel @@ -73,10 +68,10 @@ class MainActivity : private lateinit var eventManager: EventManager private lateinit var flutterNavigation: MethodChannel private lateinit var accountInitDialog: AlertDialog - private lateinit var notifications: NotificationHelper - private lateinit var receiver: NotificationReceiver - private var autoUpdateJob: Job? = null + private lateinit var surveyHelper: SurveyHelper + private val lanternServiceConnection = LanternConnection(false) + private val vpnServiceManager by lazy { VpnServiceManager(this, vpnModel) } private val lanternClient = LanternApp.getLanternHttpClient() override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { @@ -86,8 +81,6 @@ class MainActivity : vpnModel = VpnModel(this, flutterEngine, ::switchLantern) sessionModel = SessionModel(this, flutterEngine) replicaModel = ReplicaModel(this, flutterEngine) - receiver = NotificationReceiver() - notifications = NotificationHelper(this, receiver) eventManager = object : EventManager("lantern_event_channel", flutterEngine) { override fun onListen(event: Event) { if (LanternApp.getSession().lanternDidStart()) { @@ -101,11 +94,7 @@ class MainActivity : LanternApp.getSession().dnsDetector.publishNetworkAvailability() } } - - MethodChannel( - flutterEngine.dartExecutor.binaryMessenger, - "lantern_method_channel", - ).setMethodCallHandler(this) + surveyHelper = SurveyHelper(this, flutterEngine, eventManager) flutterNavigation = MethodChannel( flutterEngine.dartExecutor.binaryMessenger, @@ -135,11 +124,8 @@ class MainActivity : if (!EventBus.getDefault().isRegistered(this)) { EventBus.getDefault().register(this) } - Logger.debug(TAG, "EventBus.register finished at ${System.currentTimeMillis() - start}") - - val intent = Intent(this, LanternService_::class.java) - context.startService(intent) - Logger.debug(TAG, "startService finished at ${System.currentTimeMillis() - start}") + lanternServiceConnection.connect(this) + LanternApp.startService(lanternServiceConnection) } override fun onNewIntent(intent: Intent) { @@ -157,24 +143,6 @@ class MainActivity : } } - @SuppressLint("WrongConstant") - override fun onStart() { - super.onStart() - val packageName = activity.packageName - IntentFilter("$packageName.intent.VPN_DISCONNECTED").also { - ContextCompat.registerReceiver( - this@MainActivity, - receiver, - it, - ContextCompat.RECEIVER_NOT_EXPORTED) - } - } - - override fun onStop() { - super.onStop() - unregisterReceiver(receiver) - } - override fun onResume() { val start = System.currentTimeMillis() @@ -203,28 +171,11 @@ class MainActivity : vpnModel.destroy() sessionModel.destroy() replicaModel.destroy() + lanternServiceConnection.disconnect(this) + vpnServiceManager.destroy() EventBus.getDefault().unregister(this) } - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - when (call.method) { - "showLastSurvey" -> { - showSurvey(lastSurvey) - result.success(true) - } - - else -> result.notImplemented() - } - } - - /** - * Fetch the latest loconf config and update the UI based on those - * settings - */ - private fun fetchLoConf() { - fetch { loconf -> runOnUiThread { processLoconf(loconf) } } - } - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) fun onInitializingAccount(status: AccountInitializationStatus) { val appName = getString(R.string.app_name) @@ -272,11 +223,6 @@ class MainActivity : } } - @Subscribe(threadMode = ThreadMode.MAIN) - fun vpnStateChanged(state: VpnState) { - updateStatus(state.useVpn) - } - @Subscribe(threadMode = ThreadMode.MAIN) fun lanternStarted(status: LanternStatus) { updateUserData() @@ -338,20 +284,22 @@ class MainActivity : } private fun updatePlans() { - lanternClient.plans(object : PlansCallback { - override fun onFailure(throwable: Throwable?, error: ProError?) { - Logger.error(TAG, "Unable to fetch user plans: $error", throwable) - } - - override fun onSuccess(proPlans: Map) { - Logger.debug(TAG, "Successfully fetched plans") - for (planId in proPlans.keys) { - proPlans[planId]?.let { PlansUtil.updatePrice(activity, it) } + lanternClient.plans( + object : PlansCallback { + override fun onFailure(throwable: Throwable?, error: ProError?) { + Logger.error(TAG, "Unable to fetch user plans: $error", throwable) } - LanternApp.getSession().setUserPlans(proPlans) - } - }, null) + override fun onSuccess(proPlans: Map) { + Logger.debug(TAG, "Successfully fetched plans") + for (planId in proPlans.keys) { + proPlans[planId]?.let { PlansUtil.updatePrice(activity, it) } + } + LanternApp.getSession().setUserPlans(proPlans) + } + }, + null, + ) } private fun updatePaymentMethods() { @@ -360,106 +308,24 @@ class MainActivity : Logger.error(TAG, "Unable to fetch payment methods: $error", throwable) } - override fun onSuccess( - proPlans: Map, - paymentMethods: List - ) { - Logger.debug(TAG, "Successfully fetched payment methods") - LanternApp.getSession().setPaymentMethods(paymentMethods) - + override fun onSuccess(proPlans: Map, paymentMethods: List) { + Logger.debug(TAG, "Successfully fetched payment methods") + LanternApp.getSession().setPaymentMethods(paymentMethods) } }, null) } - @Subscribe(threadMode = ThreadMode.MAIN) - fun processLoconf(loconf: LoConf) { - doProcessLoconf(loconf) - } - - private fun doProcessLoconf(loconf: LoConf) { - val locale = LanternApp.getSession().language - val countryCode = LanternApp.getSession().countryCode - Logger.debug( - SURVEY_TAG, - "Processing loconf; country code is $countryCode", - ) - if (loconf.surveys == null) { - Logger.debug(SURVEY_TAG, "No survey config") - return - } - for (key in loconf.surveys!!.keys) { - Logger.debug(SURVEY_TAG, "Survey: " + loconf.surveys!![key]) - } - var key = countryCode - var survey = loconf.surveys!![key] - if (survey == null) { - key = countryCode.toLowerCase() - survey = loconf.surveys!![key] - } - if (survey == null || !survey.enabled) { - key = locale - survey = loconf.surveys!![key] - } - if (survey == null) { - Logger.debug(SURVEY_TAG, "No survey found") - } else if (!survey.enabled) { - Logger.debug(SURVEY_TAG, "Survey disabled") - } else if (Math.random() > survey.probability) { - Logger.debug(SURVEY_TAG, "Not showing survey this time") - } else { - Logger.debug( - SURVEY_TAG, - "Deciding whether to show survey for '%s' at %s", - key, - survey.url, - ) - val userType = survey.userType - if (userType != null) { - if (userType == "free" && LanternApp.getSession().isProUser()) { - Logger.debug( - SURVEY_TAG, - "Not showing messages targetted to free users to Pro users", - ) - return - } else if (userType == "pro" && !LanternApp.getSession().isProUser()) { - Logger.debug( - SURVEY_TAG, - "Not showing messages targetted to free users to Pro users", - ) - return - } - } - showSurveySnackbar(survey) - } - } - - fun showSurveySnackbar(survey: Survey) { - val url = survey.url - if (url != null && url != "") { - if (LanternApp.getSession().surveyLinkOpened(url)) { - Logger.debug( - TAG, - "User already opened link to survey; not displaying snackbar", - ) - return - } - } - lastSurvey = survey - Logger.debug(TAG, "Showing user survey snackbar") - eventManager.onNewEvent( - Event.SurveyAvailable, - hashMapOf("message" to survey.message, "buttonText" to survey.button), - ) + /** + * Fetch the latest loconf config and update the UI based on those + * settings + */ + private fun fetchLoConf() { + fetch { loconf -> runOnUiThread { processLoconf(loconf) } } } - private var lastSurvey: Survey? = null - - private fun showSurvey(survey: Survey?) { - survey ?: return - val intent = Intent(this, WebViewActivity_::class.java) - intent.putExtra("url", survey.url!!) - startActivity(intent) - LanternApp.getSession().setSurveyLinkOpened(survey.url) + @Subscribe(threadMode = ThreadMode.MAIN) + fun processLoconf(loconf: LoConf) { + surveyHelper.processLoconf(loconf) } @Throws(Exception::class) @@ -541,30 +407,20 @@ class MainActivity : "Load VPN configuration", ) val intent = VpnService.prepare(this) - if (intent != null) { - Logger.warn( - TAG, - "Requesting VPN connection", - ) - startActivityForResult(intent, REQUEST_VPN) + if (intent == null) { + vpnServiceManager.onVpnPermissionResult(true) } else { - Logger.debug( - TAG, - "VPN enabled, starting Lantern...", - ) - //If user come here it mean user has all permissions needed + // If user come here it mean user has all permissions needed // Also user given permission for VPN service dialog as well LanternApp.getSession().setHasFirstSessionCompleted(true) sessionModel.checkAdsAvailability() - updateStatus(true) - startVpnService() + startActivityForResult(intent, REQUEST_VPN) } } else { - sendBroadcast(notifications.disconnectIntent()) + vpnServiceManager.disconnect() } } - override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, @@ -610,38 +466,7 @@ class MainActivity : override fun onActivityResult(request: Int, response: Int, data: Intent?) { super.onActivityResult(request, response, data) - if (request == REQUEST_VPN) { - val useVpn = response == RESULT_OK - updateStatus(useVpn) - if (useVpn) { - startVpnService() - //This check is for new user that will start app first time - // this mean user has already given - // system permissions - LanternApp.getSession().setHasFirstSessionCompleted(true) - sessionModel.checkAdsAvailability() - } - - } - } - - private fun startVpnService() { - val intent: Intent = Intent( - this, - LanternVpnService::class.java, - ).apply { - action = LanternVpnService.ACTION_CONNECT - } - - startService(intent) - notifications.vpnConnectedNotification() - } - - private fun updateStatus(useVpn: Boolean) { - Logger.d(TAG, "Updating VPN status to %1\$s", useVpn) - LanternApp.getSession().updateVpnPreference(useVpn) - LanternApp.getSession().updateBootUpVpnPreference(useVpn) - vpnModel.setVpnOn(useVpn) + vpnServiceManager.onVpnPermissionResult(response == RESULT_OK) } // Recreate the activity when the language changes @@ -652,7 +477,6 @@ class MainActivity : companion object { private val TAG = MainActivity::class.java.simpleName - private val SURVEY_TAG = "$TAG.survey" private val PERMISSIONS_TAG = "$TAG.permissions" private val FULL_PERMISSIONS_REQUEST = 8888 val RECORD_AUDIO_PERMISSIONS_REQUEST = 8889 diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/loconf/SurveyHelper.kt b/android/app/src/main/kotlin/org/getlantern/lantern/loconf/SurveyHelper.kt new file mode 100644 index 000000000..321c9e009 --- /dev/null +++ b/android/app/src/main/kotlin/org/getlantern/lantern/loconf/SurveyHelper.kt @@ -0,0 +1,129 @@ +package org.getlantern.lantern.loconf + +import android.content.Context +import android.content.Intent +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import org.getlantern.lantern.LanternApp +import org.getlantern.lantern.activity.WebViewActivity_ +import org.getlantern.lantern.event.EventManager +import org.getlantern.mobilesdk.Logger +import org.getlantern.mobilesdk.model.Event +import org.getlantern.mobilesdk.model.LoConf +import org.getlantern.mobilesdk.model.Survey + +class SurveyHelper( + private val context: Context, + private val flutterEngine: FlutterEngine, + private val eventManager: EventManager, +) : MethodChannel.MethodCallHandler { + + private var lastSurvey: Survey? = null + + init { + MethodChannel( + flutterEngine.dartExecutor.binaryMessenger, + "lantern_method_channel", + ).setMethodCallHandler(this) + } + + private fun showSurvey(survey: Survey?) { + survey ?: return + val intent = Intent(context, WebViewActivity_::class.java) + intent.putExtra("url", survey.url!!) + context.startActivity(intent) + LanternApp.getSession().setSurveyLinkOpened(survey.url) + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "showLastSurvey" -> { + showSurvey(lastSurvey) + result.success(true) + } + + else -> result.notImplemented() + } + } + + fun processLoconf(loconf: LoConf) { + val locale = LanternApp.getSession().language + val countryCode = LanternApp.getSession().countryCode + Logger.debug( + TAG, + "Processing loconf; country code is $countryCode", + ) + if (loconf.surveys == null) { + Logger.debug(TAG, "No survey config") + return + } + for (key in loconf.surveys!!.keys) { + Logger.debug(TAG, "Survey: " + loconf.surveys!![key]) + } + var key = countryCode + var survey = loconf.surveys!![key] + if (survey == null) { + key = countryCode.toLowerCase() + survey = loconf.surveys!![key] + } + if (survey == null || !survey.enabled) { + key = locale + survey = loconf.surveys!![key] + } + if (survey == null) { + Logger.debug(TAG, "No survey found") + } else if (!survey.enabled) { + Logger.debug(TAG, "Survey disabled") + } else if (Math.random() > survey.probability) { + Logger.debug(TAG, "Not showing survey this time") + } else { + Logger.debug( + TAG, + "Deciding whether to show survey for '%s' at %s", + key, + survey.url, + ) + val userType = survey.userType + if (userType != null) { + if (userType == "free" && LanternApp.getSession().isProUser) { + Logger.debug( + TAG, + "Not showing messages targetted to free users to Pro users", + ) + return + } else if (userType == "pro" && !LanternApp.getSession().isProUser) { + Logger.debug( + TAG, + "Not showing messages targetted to free users to Pro users", + ) + return + } + } + showSurveySnackbar(survey) + } + } + + fun showSurveySnackbar(survey: Survey) { + val url = survey.url + if (url != null && url != "") { + if (LanternApp.getSession().surveyLinkOpened(url)) { + Logger.debug( + TAG, + "User already opened link to survey; not displaying snackbar", + ) + return + } + } + lastSurvey = survey + Logger.debug(TAG, "Showing user survey snackbar") + eventManager.onNewEvent( + Event.SurveyAvailable, + hashMapOf("message" to survey.message, "buttonText" to survey.button), + ) + } + + companion object { + private val TAG = SurveyHelper::class.java.simpleName + } +} diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/notification/NotificationHelper.kt b/android/app/src/main/kotlin/org/getlantern/lantern/notification/NotificationHelper.kt deleted file mode 100644 index 8367b89da..000000000 --- a/android/app/src/main/kotlin/org/getlantern/lantern/notification/NotificationHelper.kt +++ /dev/null @@ -1,138 +0,0 @@ -package org.getlantern.lantern.notification - -import android.annotation.TargetApi -import android.app.Activity -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.content.ContextWrapper -import android.content.Intent -import android.graphics.Color -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.core.app.NotificationCompat -import org.getlantern.lantern.MainActivity -import org.getlantern.lantern.R - -class NotificationHelper( - private val activity: Activity, - private val receiver: NotificationReceiver -) : ContextWrapper(activity) { - - // Used to notify a user of events that happen in the background - private val manager: NotificationManager = - getSystemService(NOTIFICATION_SERVICE) as NotificationManager - - private lateinit var dataUsageNotificationChannel: NotificationChannel - private lateinit var vpnNotificationChannel: NotificationChannel - private lateinit var vpnBuilder: NotificationCompat.Builder - private lateinit var dataUsageBuilder: NotificationCompat.Builder - - - @TargetApi(Build.VERSION_CODES.O) - private fun initChannels() { - dataUsageNotificationChannel = NotificationChannel( - CHANNEL_DATA_USAGE, - DATA_USAGE_DESC, - NotificationManager.IMPORTANCE_HIGH, - ) - vpnNotificationChannel = NotificationChannel( - CHANNEL_VPN, - VPN_DESC, - NotificationManager.IMPORTANCE_HIGH, - ) - val channels: Array = arrayOf( - dataUsageNotificationChannel, - vpnNotificationChannel, - ) - channels.forEach { setChannelOptions(it) } - } - - @RequiresApi(Build.VERSION_CODES.O) - private fun setChannelOptions(notificationChannel: NotificationChannel) { - notificationChannel.enableLights(true) - notificationChannel.lightColor = Color.GREEN - notificationChannel.enableVibration(false) - notificationChannel.setSound(null, null) - manager.createNotificationChannel(notificationChannel) - } - - fun disconnectIntent(): Intent { - val packageName = activity.packageName - return Intent(activity, NotificationReceiver::class.java).apply { - action = "$packageName.intent.VPN_DISCONNECTED" - } - } - - private fun disconnectBroadcast(): PendingIntent { - // Retrieve a PendingIntent that will perform a broadcast - return PendingIntent.getBroadcast( - activity, - 0, - disconnectIntent(), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, - ) - } - - - public fun vpnConnectedNotification() { - manager.notify(VPN_CONNECTED, vpnBuilder.build()) - } - - public fun dataUsageNotification() { - manager.notify(DATA_USAGE, dataUsageBuilder.build()) - } - - fun clearNotification() { - manager.cancelAll() - } - - companion object { - private val TAG = NotificationHelper::class.java.simpleName - private const val LANTERN_NOTIFICATION = "lantern.notification" - private const val DATA_USAGE = 36 - const val VPN_CONNECTED = 37 - private const val CHANNEL_VPN = "lantern_vpn" - private const val CHANNEL_DATA_USAGE = "data_usage" - private const val VPN_DESC = "VPN" - private const val DATA_USAGE_DESC = "Data Usage" - } - - init { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - initChannels() - } - val contentIntent = PendingIntent.getActivity( - activity, - 0, - Intent(activity, MainActivity::class.java), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, - ) - - // Configure vpnBuilder - vpnBuilder = buildNotification(CHANNEL_VPN, contentIntent) - - // Configure dataUsageBuilder - dataUsageBuilder = buildNotification(CHANNEL_DATA_USAGE, contentIntent) - } - - - private fun buildNotification( - channelId: String, - contentIntent: PendingIntent - ): NotificationCompat.Builder { - return NotificationCompat.Builder(this, channelId) // Channel ID provided as parameter - .setContentTitle(activity.getString(R.string.service_connected)) - .addAction( - android.R.drawable.ic_delete, - activity.getString(R.string.disconnect), - disconnectBroadcast() - ) - .setContentIntent(contentIntent) - .setOngoing(true) - .setShowWhen(true) - .setSmallIcon(R.drawable.lantern_notification_icon) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - } -} diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/notification/NotificationReceiver.kt b/android/app/src/main/kotlin/org/getlantern/lantern/notification/NotificationReceiver.kt deleted file mode 100644 index d2cc1736e..000000000 --- a/android/app/src/main/kotlin/org/getlantern/lantern/notification/NotificationReceiver.kt +++ /dev/null @@ -1,40 +0,0 @@ -package org.getlantern.lantern.notification - -import android.app.Activity -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.app.NotificationManager -import io.lantern.model.VpnModel -import org.getlantern.lantern.LanternApp -import org.getlantern.lantern.model.Utils -import org.getlantern.lantern.model.VpnState -import org.getlantern.lantern.vpn.LanternVpnService -import org.getlantern.mobilesdk.Logger -import org.greenrobot.eventbus.EventBus - - -class NotificationReceiver() : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - Logger.debug(TAG, "Received disconnect broadcast") - val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - manager.cancel(NotificationHelper.VPN_CONNECTED) - if (Utils.isServiceRunning( - context, - LanternVpnService::class.java, - ) - ) { - EventBus.getDefault().post(VpnState(false)) - context.startService( - Intent( - context, - LanternVpnService::class.java, - ).setAction(LanternVpnService.ACTION_DISCONNECT), - ) - } - } - - companion object { - private val TAG = NotificationReceiver::class.java.simpleName - } -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/notification/Notifications.kt b/android/app/src/main/kotlin/org/getlantern/lantern/notification/Notifications.kt new file mode 100644 index 000000000..639117adb --- /dev/null +++ b/android/app/src/main/kotlin/org/getlantern/lantern/notification/Notifications.kt @@ -0,0 +1,98 @@ +package org.getlantern.lantern.notification + +import android.annotation.TargetApi +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import org.getlantern.lantern.Actions +import org.getlantern.lantern.LanternApp +import org.getlantern.lantern.MainActivity +import org.getlantern.lantern.R + +class Notifications() { + companion object { + private val TAG = Notifications::class.java.simpleName + const val notificationId = 1 + private const val CHANNEL_VPN = "service-vpn" + private const val CHANNEL_SERVICE = "service-lantern" + + init { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + initChannels() + } + } + + val serviceBuilder: (service: Service) -> NotificationCompat.Builder by lazy { + { + val context = it as Context + NotificationCompat.Builder(it as Context, CHANNEL_SERVICE) + .setContentTitle(context.getString(R.string.service_connected)) + .setShowWhen(false) + } + } + + val builder: (service: Service) -> NotificationCompat.Builder by lazy { + { + val context = it as Context + val pendingIntent = PendingIntent.getActivity( + context, + 0, + Intent(context, MainActivity::class.java), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + NotificationCompat.Builder(it as Context, CHANNEL_VPN) + .setContentTitle(context.getString(R.string.service_connected)) + .setContentIntent(pendingIntent) + .addAction( + android.R.drawable.ic_delete, + context.getString(R.string.disconnect), + PendingIntent.getBroadcast( + context, + 0, + Intent(Actions.DISCONNECT_VPN).setPackage(it.packageName), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ), + ) + .setOngoing(true) + .setShowWhen(true) + .setSmallIcon(R.drawable.lantern_notification_icon) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + } + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun createChannel(notificationChannel: NotificationChannel) { + notificationChannel.enableLights(true) + notificationChannel.lightColor = Color.GREEN + notificationChannel.enableVibration(false) + notificationChannel.setSound(null, null) + LanternApp.notificationManager.createNotificationChannel(notificationChannel) + } + + @TargetApi(Build.VERSION_CODES.O) + private fun initChannels() { + val vpnNotificationChannel = NotificationChannel( + CHANNEL_VPN, + LanternApp.getAppContext().resources.getString(R.string.lantern_service), + NotificationManager.IMPORTANCE_HIGH, + ) + val serviceNotificationChannel = NotificationChannel( + CHANNEL_SERVICE, + LanternApp.getAppContext().resources.getString(R.string.lantern_service), + NotificationManager.IMPORTANCE_LOW, + ) + val channels: Array = arrayOf( + serviceNotificationChannel, + vpnNotificationChannel, + ) + channels.forEach { createChannel(it) } + } + } +} diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/notification/Notifier.kt b/android/app/src/main/kotlin/org/getlantern/lantern/notification/Notifier.kt deleted file mode 100644 index 2373b760a..000000000 --- a/android/app/src/main/kotlin/org/getlantern/lantern/notification/Notifier.kt +++ /dev/null @@ -1,122 +0,0 @@ -package org.getlantern.lantern.notification - -import android.annotation.TargetApi -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.res.Resources -import android.graphics.Bitmap -import android.graphics.Canvas -import android.os.Build -import androidx.core.app.NotificationCompat -import org.getlantern.lantern.BuildConfig -import org.getlantern.lantern.LanternApp -import org.getlantern.lantern.MainActivity -import org.getlantern.lantern.R -import org.getlantern.mobilesdk.Logger - -/** - * Handles notifications. - */ -class Notifier : BroadcastReceiver() { - - override fun onReceive(context: Context, intent: Intent) { - - Logger.i(TAG, "Notifying" + intent.action) - - val notificationId: Int - val resources = context.resources - - // See http://developer.android.com/guide/topics/ui/notifiers/notifications.html - val builder = NotificationCompat.Builder(context) - .setContentTitle(resources.getString(R.string.lantern_notification)) - .setAutoCancel(true) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - builder.setSmallIcon(R.drawable.icon_alert_white) - } else { - builder.setSmallIcon(R.drawable.notification_icon) - } - - when (intent.action) { - - ACTION_DATA_USAGE -> { - notificationId = NOTIFICATION_ID_DATA_USAGE - val text = intent.getStringExtra(EXTRA_TEXT) - - builder.setChannelId(CHANNEL_DATA_USAGE) - builder.setContentText(text) - builder.setStyle(NotificationCompat.BigTextStyle().bigText(text)) - } - - else -> { - Logger.debug(TAG, "Got invalid broadcast " + intent.action) - return - } - } - - val resultIntent = Intent(context, MainActivity::class.java) - - // For unknown reason, passing this (instead of zero) resumes the - // existing activity if possible, instead of creating a new one. - val requestCode = System.currentTimeMillis().toInt() - val resultPendingIntent = PendingIntent.getActivity( - context, - requestCode, - resultIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - builder.setContentIntent(resultPendingIntent) - val notification = builder.build() - - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.notify(notificationId, notification) - } - - /** - * See https://android.googlesource.com/platform/packages/experimental/+/363b69b578809b2d5f7ea49d186197797590fac4/NotificationShowcase/src/com/android/example/notificationshowcase/NotificationShowcaseActivity.java for example. - */ - private fun getBitmap(resources: Resources, iconId: Int): Bitmap { - val width = resources.getDimension(android.R.dimen.notification_large_icon_width).toInt() - val height = resources.getDimension(android.R.dimen.notification_large_icon_height).toInt() - val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) - val c = Canvas(bitmap) - val d = resources.getDrawable(iconId) - d.setBounds(0, 0, width, height) - d.draw(c) - return bitmap - } - - companion object { - private const val TAG = "Notifier" - - const val CHANNEL_DATA_USAGE = "Data Usage" - - const val ACTION_DATA_USAGE = BuildConfig.APPLICATION_ID + ".intent.DATA_USAGE" - - const val NOTIFICATION_ID_DATA_USAGE = 100 - - const val EXTRA_TEXT = "text" - - init { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - createNotificationChannels() - } - } - - @TargetApi(Build.VERSION_CODES.O) - private fun createNotificationChannels() { - val notificationManager = LanternApp.getAppContext().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - - // create a channel for data usage notifications - notificationManager.createNotificationChannel( - NotificationChannel(CHANNEL_DATA_USAGE, CHANNEL_DATA_USAGE, NotificationManager.IMPORTANCE_DEFAULT) - ) - - // create other notification channels - } - } -} diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/service/AutoStarter.kt b/android/app/src/main/kotlin/org/getlantern/lantern/service/AutoStarter.kt deleted file mode 100644 index 0c724f126..000000000 --- a/android/app/src/main/kotlin/org/getlantern/lantern/service/AutoStarter.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.getlantern.lantern.service - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import org.getlantern.mobilesdk.Logger - -open class AutoStarter : BroadcastReceiver() { - - override fun onReceive(context: Context, intent: Intent) { - Logger.d(TAG, "Automatically starting Lantern Service on: ${intent.getAction()}") - val serviceIntent = Intent(context, LanternService_::class.java) - .putExtra(LanternService.AUTO_BOOTED, intent.getAction() == Intent.ACTION_BOOT_COMPLETED) - context.startService(serviceIntent) - } - - companion object { - private val TAG = AutoStarter::class.java.simpleName - } -} diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/service/ConnectionState.kt b/android/app/src/main/kotlin/org/getlantern/lantern/service/ConnectionState.kt new file mode 100644 index 000000000..8cc7d11d9 --- /dev/null +++ b/android/app/src/main/kotlin/org/getlantern/lantern/service/ConnectionState.kt @@ -0,0 +1,11 @@ +package org.getlantern.lantern.service + +enum class ConnectionState( + val started: Boolean = false, + val connected: Boolean = false, +) { + Connecting(true, false), + Connected(true, true), + Disconnecting, + Disconnected, +} diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/service/LanternConnection.kt b/android/app/src/main/kotlin/org/getlantern/lantern/service/LanternConnection.kt new file mode 100644 index 000000000..406c6c996 --- /dev/null +++ b/android/app/src/main/kotlin/org/getlantern/lantern/service/LanternConnection.kt @@ -0,0 +1,56 @@ +package org.getlantern.lantern.service + +import android.app.Service +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import org.getlantern.lantern.vpn.LanternVpnService + +class LanternConnection(val isVpnService: Boolean = false) : ServiceConnection { + private var binder: IBinder? = null + + // private var callback: Callback? = null + private var connectionActive = false + var service: Service? = null + + val serviceClass + get() = when (isVpnService) { + true -> LanternVpnService::class + else -> LanternService::class + }.java + + override fun onServiceConnected(name: ComponentName?, binder: IBinder) { + this.binder = binder + this.service = if ((binder as? LanternVpnService) != null) { + (binder as LanternVpnService.LocalBinder).service + } else { + (binder as LanternService.LocalBinder).service + } + } + + override fun onServiceDisconnected(name: ComponentName?) { + service = null + binder = null + } + + fun connect(context: Context) { + if (connectionActive) return + connectionActive = true + val intent = Intent(context, serviceClass) + context.bindService(intent, this, Context.BIND_AUTO_CREATE) + } + + fun disconnect(context: Context) { + if (connectionActive) { + try { + context.unbindService(this) + } catch (_: IllegalArgumentException) { + } + } + connectionActive = false + binder = null + service = null + } +} diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/service/LanternService.kt b/android/app/src/main/kotlin/org/getlantern/lantern/service/LanternService.kt index a55cd84b1..e75ed986a 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/service/LanternService.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/service/LanternService.kt @@ -2,23 +2,23 @@ package org.getlantern.lantern.service import android.app.Service import android.content.Intent -import android.os.Handler +import android.os.Binder +import android.os.Build import android.os.IBinder -import android.os.Looper -import androidx.annotation.Nullable import com.google.gson.JsonObject import okhttp3.HttpUrl import okhttp3.Response -import org.androidannotations.annotations.EService import org.getlantern.lantern.BuildConfig import org.getlantern.lantern.LanternApp import org.getlantern.lantern.R import org.getlantern.lantern.model.AccountInitializationStatus import org.getlantern.lantern.model.LanternHttpClient +import org.getlantern.lantern.model.LanternHttpClient.ProCallback import org.getlantern.lantern.model.LanternStatus import org.getlantern.lantern.model.LanternStatus.Status import org.getlantern.lantern.model.ProError import org.getlantern.lantern.model.ProUser +import org.getlantern.lantern.notification.Notifications import org.getlantern.lantern.util.AutoUpdater import org.getlantern.lantern.util.Json import org.getlantern.mobilesdk.Lantern @@ -28,173 +28,106 @@ import org.getlantern.mobilesdk.StartResult import org.getlantern.mobilesdk.model.LoConf import org.getlantern.mobilesdk.model.LoConfCallback import org.greenrobot.eventbus.EventBus -import java.util.Random -import java.util.concurrent.atomic.AtomicBoolean -@EService -open class LanternService : Service(), Runnable { +class LanternService : Service(), ServiceManager.Runner { companion object { private val TAG = LanternService::class.java.simpleName - private const val MAX_CREATE_USER_TRIES = 11 - private const val baseWaitMs = 3000 private val lanternClient: LanternHttpClient = LanternApp.getLanternHttpClient() - public val AUTO_BOOTED = "autoBooted" } - private var thread: Thread? = null - private val createUserHandler: Handler = Handler(Looper.getMainLooper()) - private val createUserRunnable: CreateUser = CreateUser(this) - private val random: Random = Random() - private val serviceIcon: Int = if (LanternApp.getSession().chatEnabled()) { - R.drawable.status_chat - } else { - R.drawable.status_plain - } - private val helper: ServiceHelper = ServiceHelper(this, serviceIcon, R.string.ready_to_connect) - private val started: AtomicBoolean = AtomicBoolean() - private lateinit var autoUpdater: AutoUpdater - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - if (intent == null) return START_NOT_STICKY - autoUpdater = AutoUpdater(this) - val autoBooted = intent.getBooleanExtra(AUTO_BOOTED, false) - Logger.d(TAG, "Called onStartCommand, autoBooted?: $autoBooted") - if (autoBooted) { - Logger.debug( - TAG, - "Attempted to auto boot but user has not onboarded to messaging, stop LanternService", - ) - stopSelf() - return START_NOT_STICKY - } - if (started.compareAndSet(false, true)) { - Logger.d(TAG, "Starting Lantern service thread") - thread = Thread(this, "LanternService") - thread?.start() - } + private var autoUpdater: AutoUpdater = AutoUpdater(this) - return super.onStartCommand(intent, flags, startId) + inner class LocalBinder : Binder() { + val service + get() = this@LanternService } - @Nullable - override fun onBind(intent: Intent): IBinder? { - return null - } + override val data = ServiceManager.Data(this) - override fun run() { - // move the current thread of the service to the background - android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND) + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int = + super.onStartCommand(intent, flags, startId) + override suspend fun startProcesses() { val locale = LanternApp.getSession().language val settings = LanternApp.getSession().settings try { + if (Build.VERSION.SDK_INT >= 26) serviceNotification() Logger.debug(TAG, "Successfully loaded config: $settings") val result: StartResult = Lantern.enable(this, locale, settings, LanternApp.getSession()) LanternApp.getSession().setStartResult(result) - afterStart() + // create a user if no user id is stored + if (LanternApp.getSession().userId().toInt() == 0) createUser() + EventBus.getDefault().postSticky(LanternStatus(Status.ON)) + // fetch latest loconf + LoConf.Companion.fetch(object : LoConfCallback { + override fun onSuccess(loconf: LoConf) { + EventBus.getDefault().post(loconf) + } + }) + + if (!BuildConfig.PLAY_VERSION && !BuildConfig.DEVELOPMENT_MODE) { + // check if an update is available + autoUpdater.checkForUpdates() + } } catch (lnre: LanternNotRunningException) { Logger.e(TAG, "Unable to start LanternService", lnre) - throw RuntimeException("Could not start Lantern", lnre) - } - } - - private fun afterStart() { - if (LanternApp.getSession().userId().toInt() == 0) { - // create a user if no user id is stored - EventBus.getDefault().post( - AccountInitializationStatus(AccountInitializationStatus.Status.PROCESSING), - ) - createUser(0) - } - - if (!BuildConfig.PLAY_VERSION && !BuildConfig.DEVELOPMENT_MODE) { - // check if an update is available - autoUpdater.checkForUpdates() } - - EventBus.getDefault().postSticky(LanternStatus(Status.ON)) - - // fetch latest loconf - LoConf.Companion.fetch(object : LoConfCallback { - override fun onSuccess(loconf: LoConf) { - EventBus.getDefault().post(loconf) - } - }) } - private fun createUser(attempt: Int) { - val maxBackOffTime = 60000L // maximum backoff time in milliseconds (e.g., 1 minute) - val timeOut = - (baseWaitMs * Math.pow(2.0, attempt.toDouble())).toLong().coerceAtMost(maxBackOffTime) - createUserHandler.postDelayed(createUserRunnable, timeOut) + suspend fun createUser() { + EventBus.getDefault().post( + AccountInitializationStatus(AccountInitializationStatus.Status.PROCESSING), + ) + val url: HttpUrl = LanternHttpClient.createProUrl("/user-create") + val json: JsonObject = JsonObject() + json.addProperty("locale", LanternApp.getSession().language) + lanternClient.post( + url, + LanternHttpClient.createJsonBody(json), + object : ProCallback { + override fun onFailure(throwable: Throwable?, error: ProError?) { + Logger.error(TAG, "Unable to fetch user data: $error", throwable) + } + + override fun onSuccess(response: Response?, result: JsonObject?) { + val user: ProUser? = Json.gson.fromJson(result, ProUser::class.java) + if (user == null) { + Logger.error(TAG, "Unable to parse user from JSON") + return + } + Logger.debug(TAG, "Created new Lantern user: ${user.newUserDetails()}") + LanternApp.getSession().setUserIdAndToken( + user.userId, + user.token, + ) + val referral = user.referral + if (!referral.isEmpty()) { + LanternApp.getSession().setCode(referral) + } + EventBus.getDefault().postSticky(LanternStatus(Status.ON)) + EventBus.getDefault().postSticky( + AccountInitializationStatus( + AccountInitializationStatus.Status.SUCCESS, + ), + ) + } + }, + ) } - private class CreateUser(val service: LanternService) : Runnable, - LanternHttpClient.ProCallback { + override fun onBind(intent: Intent): IBinder? = null - private var attempts: Int = 0 - - override fun run() { - val url: HttpUrl = LanternHttpClient.createProUrl("/user-create") - val json: JsonObject = JsonObject() - json.addProperty("locale", LanternApp.getSession().language) - lanternClient.post(url, LanternHttpClient.createJsonBody(json), this) - } - - override fun onFailure(@Nullable throwable: Throwable?, @Nullable error: ProError?) { - if (attempts >= MAX_CREATE_USER_TRIES) { - Logger.error(TAG, "Max. number of tries made to create Pro user") - EventBus.getDefault().postSticky( - AccountInitializationStatus(AccountInitializationStatus.Status.FAILURE), - ) - return - } - attempts++ - service.createUser(attempts) - } - - override fun onSuccess(response: Response?, result: JsonObject?) { - val user: ProUser? = Json.gson.fromJson(result, ProUser::class.java) - if (user == null) { - Logger.error(TAG, "Unable to parse user from JSON") - return - } - service.createUserHandler.removeCallbacks(service.createUserRunnable) - Logger.debug(TAG, "Created new Lantern user: ${user.newUserDetails()}") - LanternApp.getSession().setUserIdAndToken(user.userId, user.token) - val referral = user.referral - if (!referral.isEmpty()) { - LanternApp.getSession().setCode(referral) - } - EventBus.getDefault().postSticky(LanternStatus(Status.ON)) - EventBus.getDefault().postSticky( - AccountInitializationStatus(AccountInitializationStatus.Status.SUCCESS), - ) - } + override fun killProcesses() { + Logger.d(TAG, "stop") + stopForeground(true) } - override fun onDestroy() { - super.onDestroy() - if (!started.get()) { - Logger.debug(TAG, "Service never started, exit immediately") - return - } - helper.onDestroy() - thread?.interrupt() - try { - Logger.debug(TAG, "Unregistering screen state receiver") - createUserHandler.removeCallbacks(createUserRunnable) - } catch (e: Exception) { - Logger.error(TAG, "Exception", e) - } - // We want to keep the service running as much as possible to allow receiving messages, so - // we start it back up automatically as explained at https://stackoverflow.com/a/52258125. - val broadcastIntent = Intent() - .setAction("restartservice") - .setClass(this, AutoStarter::class.java) - sendBroadcast(broadcastIntent) - } + private fun serviceNotification() = startForeground( + 1, + Notifications.serviceBuilder(this).build(), + ) + } diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/service/ServiceHelper.kt b/android/app/src/main/kotlin/org/getlantern/lantern/service/ServiceHelper.kt deleted file mode 100644 index 5b3cbba44..000000000 --- a/android/app/src/main/kotlin/org/getlantern/lantern/service/ServiceHelper.kt +++ /dev/null @@ -1,94 +0,0 @@ -@file:JvmName("ServiceHelper") - -package org.getlantern.lantern.service - -import android.annotation.TargetApi -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.app.Service -import android.content.Context -import android.content.Intent -import android.os.Build -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import org.getlantern.lantern.LanternApp -import org.getlantern.lantern.MainActivity -import org.getlantern.lantern.R -import java.util.concurrent.LinkedBlockingDeque -import java.util.concurrent.atomic.AtomicBoolean - -class ServiceHelper( - private val service: Service, - private val defaultIcon: Int, - private val defaultText: Int -) { - private val foregrounded = AtomicBoolean(false) - - fun makeForeground() { - if (foregrounded.compareAndSet(false, true)) { - val doIt = { - service.startForeground(notificationId, buildNotification(defaultIcon, defaultText)) - } - serviceDeque.push(doIt) - doIt() - } - } - - fun onDestroy() { - if (foregrounded.compareAndSet(true, false)) { - serviceDeque.pop() - // Put the prior service that was in the foreground back into the foreground - serviceDeque.peekLast()?.let { it() } - } - } - - fun updateNotification(icon: Int, text: Int) { - with(NotificationManagerCompat.from(service)) { - notify(notificationId, buildNotification(icon, text)) - } - } - - private fun buildNotification(icon: Int, text: Int): Notification { - var channelId: String? = null - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - channelId = createNotificationChannel() - } else { - // If earlier version channel ID is not used - // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context) - } - val openMainActivity = PendingIntent.getActivity( - service, 0, Intent(service, MainActivity::class.java), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE - ) - val notificationBuilder = channelId?.let { NotificationCompat.Builder(service, it) } - ?: NotificationCompat.Builder(service) - notificationBuilder.setSmallIcon(icon) - val appName = service.getText(R.string.app_name) - notificationBuilder.setContentTitle(appName) - val content = service.getText(text) - notificationBuilder.setContentText(content) - notificationBuilder.setContentIntent(openMainActivity) - notificationBuilder.setBadgeIconType(NotificationCompat.BADGE_ICON_NONE) - notificationBuilder.setNumber(0) - notificationBuilder.setOngoing(true) - return notificationBuilder.build() - } - - @TargetApi(Build.VERSION_CODES.O) - private fun createNotificationChannel(): String { - val channelId = "lantern_service" - val channelName = LanternApp.getAppContext().resources.getString(R.string.lantern_service) - val mNotificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - mNotificationManager.createNotificationChannel( - NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_DEFAULT) - ) - return channelId - } - - companion object ServiceHelper { - private const val notificationId = 1 - private val serviceDeque = LinkedBlockingDeque<() -> Unit>() - } -} diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/service/ServiceManager.kt b/android/app/src/main/kotlin/org/getlantern/lantern/service/ServiceManager.kt new file mode 100644 index 000000000..d77a5f59d --- /dev/null +++ b/android/app/src/main/kotlin/org/getlantern/lantern/service/ServiceManager.kt @@ -0,0 +1,103 @@ +package org.getlantern.lantern.service + +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import kotlinx.coroutines.* +import org.getlantern.lantern.Actions +import org.getlantern.lantern.util.broadcastReceiver +import org.getlantern.lantern.util.runOnMainDispatcher +import org.getlantern.mobilesdk.Logger + +class ServiceManager { + + companion object { + private val TAG = ServiceManager::class.java.simpleName + } + + class Data internal constructor(private val service: Runner) { + var state = ConnectionState.Disconnected + var connectingJob: Job? = null + + val stopReceiver = broadcastReceiver { _, _ -> + service.stopRunner() + } + + fun changeState(s: ConnectionState) { + state = s + } + + var closeReceiverRegistered = false + } + + interface Runner { + val data: Data + fun killProcesses() + suspend fun startProcesses() + + fun startRunner() { + this as Context + if (Build.VERSION.SDK_INT >= 26) { + startForegroundService(Intent(this, javaClass)) + } else { + startService(Intent(this, javaClass)) + } + } + + fun stopRunner(restart: Boolean = false) { + if (data.state == ConnectionState.Disconnecting) return + this as Service + Logger.d(TAG, "Received stop service request") + data.changeState(ConnectionState.Disconnecting) + runOnMainDispatcher { + data.connectingJob?.cancelAndJoin() + coroutineScope { + killProcesses() + val data = data + if (data.closeReceiverRegistered) { + unregisterReceiver(data.stopReceiver) + data.closeReceiverRegistered = false + } + } + + data.changeState(ConnectionState.Disconnected) + if (restart) { + startRunner() + } else { + stopSelf() + } + } + } + + fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val data = data + if (data.state != ConnectionState.Disconnected) return Service.START_NOT_STICKY + this as Context + if (!data.closeReceiverRegistered) { + registerReceiver( + data.stopReceiver, + IntentFilter().apply { + addAction(Actions.STOP_SERVICE) + }, + ) + data.closeReceiverRegistered = true + } + + data.changeState(ConnectionState.Connecting) + runOnMainDispatcher { + try { + startProcesses() + data.changeState(ConnectionState.Connected) + } catch (exc: Throwable) { + stopRunner() + Logger.d(TAG, "Failed to start service: ") + } finally { + data.connectingJob = null + } + } + return Service.START_NOT_STICKY + } + } +} diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/util/ActivityExt.kt b/android/app/src/main/kotlin/org/getlantern/lantern/util/ActivityExt.kt index 324778773..eb2c2ce78 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/util/ActivityExt.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/util/ActivityExt.kt @@ -1,6 +1,7 @@ package org.getlantern.lantern.util import android.app.Activity +import android.app.ActivityManager import android.app.AlarmManager import android.app.PendingIntent import android.content.Context @@ -26,6 +27,14 @@ fun Activity.showErrorDialog( ) } +fun Context.isServiceRunning(serviceClass: Class<*>): Boolean { + val manager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + for (service in manager.getRunningServices(Int.MAX_VALUE)) + if (serviceClass.name == service.service.className) + return true + return false +} + fun Activity.openHome() { startActivity( Intent(this, MainActivity::class.java) diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/util/Utils.kt b/android/app/src/main/kotlin/org/getlantern/lantern/util/Utils.kt new file mode 100644 index 000000000..e4cc3b36a --- /dev/null +++ b/android/app/src/main/kotlin/org/getlantern/lantern/util/Utils.kt @@ -0,0 +1,15 @@ +package org.getlantern.lantern.util + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import kotlinx.coroutines.* + +fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) = callback(context, intent) + } + +fun runOnMainDispatcher(block: suspend CoroutineScope.() -> Unit) = + GlobalScope.launch(Dispatchers.Main.immediate, block = block) + diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/vpn/GoTun2SocksProvider.kt b/android/app/src/main/kotlin/org/getlantern/lantern/vpn/GoTun2SocksProvider.kt deleted file mode 100644 index 5c62def0d..000000000 --- a/android/app/src/main/kotlin/org/getlantern/lantern/vpn/GoTun2SocksProvider.kt +++ /dev/null @@ -1,135 +0,0 @@ -package org.getlantern.lantern.vpn - -import android.app.PendingIntent -import android.content.Intent -import android.content.pm.PackageManager -import android.net.VpnService -import android.os.ParcelFileDescriptor -import internalsdk.Internalsdk -import org.getlantern.lantern.LanternApp -import org.getlantern.lantern.MainActivity -import org.getlantern.mobilesdk.Logger -import org.getlantern.mobilesdk.model.SessionManager -import java.util.Locale - -class GoTun2SocksProvider( - val packageManager: PackageManager, - val splitTunnelingEnabled: Boolean, - val appsAllowedAccess: Set, -) : Provider { - - companion object { - private val TAG = GoTun2SocksProvider::class.java.simpleName - private const val sessionName = "LanternVpn" - private const val privateAddress = "10.0.0.2" - private const val VPN_MTU = 1500 - } - - private var mInterface: ParcelFileDescriptor? = null - - @Synchronized - private fun createBuilder( - vpnService: VpnService, - builder: VpnService.Builder, - ): ParcelFileDescriptor? { - // Set the locale to English - // since the VpnBuilder encounters - // issues with non-English numerals - // See https://code.google.com/p/android/issues/detail?id=61096 - Locale.setDefault(Locale("en")) - - // Configure a builder while parsing the parameters. - builder.setMtu(VPN_MTU) - builder.addAddress(privateAddress, 24) - // route IPv4 through VPN - builder.addRoute("0.0.0.0", 0) - - if (splitTunnelingEnabled) { - // Exclude any app that's not in our split tunneling allowed list - for (installedApp in packageManager.getInstalledApplications(0)) { - if (!appsAllowedAccess.contains(installedApp.packageName)) { - try { - Logger.debug(TAG, "Excluding " + installedApp.packageName + " from VPN") - builder.addDisallowedApplication(installedApp.packageName) - } catch (e: PackageManager.NameNotFoundException) { - throw RuntimeException("Unable to exclude " + installedApp.packageName + " from VPN", e) - } - } - } - } - - // Never capture traffic originating from Lantern itself in the VPN. - try { - val ourPackageName = vpnService.getPackageName() - builder.addDisallowedApplication(ourPackageName) - } catch (e: PackageManager.NameNotFoundException) { - throw RuntimeException("Unable to exclude Lantern from routes", e) - } - // don't currently route IPv6 through VPN because our proxies don't currently support IPv6 - // see https://github.com/getlantern/lantern-internal/issues/4961 - // Note - if someone performs a DNS lookup for an IPv6 only host like ipv6.google.com, dnsgrab - // will return an IPv4 address for that site, causing the traffic to get routed through the VPN. - // builder.addRoute("0:0:0:0:0:0:0:0", 0) - - // this is a fake DNS server. The precise IP doesn't matter because Lantern will intercept and - // route all DNS traffic to dnsgrab internally anyway. - builder.addDnsServer(SessionManager.fakeDnsIP) - - val intent = Intent(vpnService, MainActivity::class.java) - val pendingIntent: PendingIntent = PendingIntent.getActivity( - vpnService, - 0, - intent, - PendingIntent.FLAG_IMMUTABLE, - ) - builder.setConfigureIntent(pendingIntent) - - builder.setSession(sessionName) - - // Create a new mInterface using the builder and save the parameters. - mInterface = builder.establish() - Logger.d(TAG, "New mInterface: " + mInterface) - return mInterface - } - - override fun run( - vpnService: VpnService, - builder: VpnService.Builder, - socksAddr: String, - dnsGrabAddr: String, - ) { - Logger.d(TAG, "run") - - val defaultLocale = Locale.getDefault() - try { - Logger.debug(TAG, "Creating VpnBuilder before starting tun2socks") - val intf: ParcelFileDescriptor? = createBuilder(vpnService, builder) - Logger.debug(TAG, "Running tun2socks") - if (intf != null) { - Internalsdk.tun2Socks( - intf.getFd().toLong(), - socksAddr, - dnsGrabAddr, - VPN_MTU.toLong(), - LanternApp.getSession(), - ) - } - } catch (t: Throwable) { - Logger.e(TAG, "Exception while handling TUN device", t) - } finally { - Locale.setDefault(defaultLocale) - } - } - - @Synchronized - @Throws(Exception::class) - override fun stop() { - Logger.d(TAG, "stop") - Internalsdk.stopTun2Socks() - mInterface?.let { - Logger.d(TAG, "closing interface") - mInterface!!.close() - mInterface = null - } - } -} diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/vpn/LanternVpnService.kt b/android/app/src/main/kotlin/org/getlantern/lantern/vpn/LanternVpnService.kt index c703c75d2..08b0bc7dc 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/vpn/LanternVpnService.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/vpn/LanternVpnService.kt @@ -1,131 +1,151 @@ package org.getlantern.lantern.vpn -import android.content.ComponentName -import android.content.Context +import android.app.PendingIntent import android.content.Intent -import android.content.ServiceConnection +import android.content.pm.PackageManager import android.net.VpnService -import android.os.IBinder +import android.os.Binder +import android.os.Build +import android.os.ParcelFileDescriptor +import internalsdk.Internalsdk import org.getlantern.lantern.LanternApp -import org.getlantern.lantern.plausible.Plausible -import org.getlantern.lantern.service.LanternService_ +import org.getlantern.lantern.MainActivity +import org.getlantern.lantern.notification.Notifications +import org.getlantern.lantern.service.ServiceManager import org.getlantern.mobilesdk.Logger +import org.getlantern.mobilesdk.model.SessionManager +import java.util.Locale -class LanternVpnService : VpnService(), Runnable { +class LanternVpnService : VpnService(), ServiceManager.Runner { companion object { - const val ACTION_CONNECT = "org.getlantern.lantern.vpn.START" - const val ACTION_DISCONNECT = "org.getlantern.lantern.vpn.STOP" private val TAG = LanternVpnService::class.java.simpleName + private const val sessionName = "LanternVpn" + private const val privateAddress = "10.0.0.2" + private const val VPN_MTU = 1500 } - private var provider: Provider? = null + lateinit var conn: ParcelFileDescriptor + private val binder = LocalBinder() + override val data = ServiceManager.Data(this) - private val lanternServiceConnection: ServiceConnection = object : ServiceConnection { - override fun onServiceDisconnected(name: ComponentName) { - Logger.e(TAG, "LanternService disconnected, disconnecting VPN") - stop() - } - override fun onServiceConnected(name: ComponentName, service: IBinder) {} + inner class LocalBinder : Binder() { + val service + get() = this@LanternVpnService } - override fun onCreate() { - super.onCreate() - Logger.d(TAG, "VpnService created") - bindService( - Intent(this, LanternService_::class.java), - lanternServiceConnection, - Context.BIND_AUTO_CREATE, - ) - } + override fun onRevoke() = stopRunner() - override fun onDestroy() { - Logger.d(TAG, "destroyed") - doStop() - super.onDestroy() - unbindService(lanternServiceConnection) + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + return super.onStartCommand(intent, flags, startId) } - override fun onRevoke() { - Logger.d(TAG, "revoked") - stop() - } + override suspend fun startProcesses() = startVpn() + + @Synchronized + private fun createBuilder(): ParcelFileDescriptor? { + val builder = Builder() + // Set the locale to English + // since the VpnBuilder encounters + // issues with non-English numerals + // See https://code.google.com/p/android/issues/detail?id=61096 + Locale.setDefault(Locale("en")) + + // Configure a builder while parsing the parameters. + builder.setMtu(VPN_MTU) + builder.addAddress(privateAddress, 24) + // route IPv4 through VPN + builder.addRoute("0.0.0.0", 0) - override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { - // Somehow we are getting null here when running on Android 5.0 - // Handling null intent scenario - if (intent == null) { - Logger.d(TAG, "LanternVpnService: Received null intent, service is being restarted") - return START_STICKY + if (LanternApp.getSession().splitTunnelingEnabled()) { + val appsAllowedAccess = HashSet(LanternApp.getSession().appsAllowedAccess()) + // Exclude any app that's not in our split tunneling allowed list + for (installedApp in packageManager.getInstalledApplications(0)) { + if (!appsAllowedAccess.contains(installedApp.packageName)) { + try { + Logger.debug(TAG, "Excluding " + installedApp.packageName + + " from VPN") + builder.addDisallowedApplication(installedApp.packageName) + } catch (e: PackageManager.NameNotFoundException) { + throw RuntimeException("Unable to exclude " + + installedApp.packageName + " from VPN", e) + } + } + } } - return if (intent.action == ACTION_DISCONNECT) { - Plausible.event("switchVPN", "", "", mapOf("status" to "disconnect")) - stop() - START_NOT_STICKY - } else { - LanternApp.getSession().updateVpnPreference(true) - connect() - START_STICKY + + // Never capture traffic originating from Lantern itself in the VPN. + try { + val ourPackageName = getPackageName() + builder.addDisallowedApplication(ourPackageName) + } catch (e: PackageManager.NameNotFoundException) { + throw RuntimeException("Unable to exclude Lantern from routes", e) } - } + // don't currently route IPv6 through VPN because our proxies don't currently + // support IPv6. see https://github.com/getlantern/lantern-internal/issues/4961 + // Note - if someone performs a DNS lookup for an IPv6 only host like + // ipv6.google.com, dnsgrab will return an IPv4 address for that site, causing + // the traffic to get routed through the VPN. + + // this is a fake DNS server. The precise IP doesn't matter because Lantern will + // intercept and route all DNS traffic to dnsgrab internally anyway. + builder.addDnsServer(SessionManager.fakeDnsIP) + + val intent = Intent(this, MainActivity::class.java) + val pendingIntent: PendingIntent = PendingIntent.getActivity( + this, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE, + ) + builder.setConfigureIntent(pendingIntent) + + builder.setSession(sessionName) - private fun connect() { - Logger.d(TAG, "connect") - Plausible.event("switchVPN", "", "", mapOf("status" to "connect")) - Thread(this, "VpnService").start() + // Create a new mInterface using the builder and save the parameters. + return builder.establish() } - override fun run() { + private fun serviceNotification() = startForeground(1, + Notifications.builder(this).build()) + + private fun startVpn() { + val builder = Builder() + val defaultLocale = Locale.getDefault() try { - Logger.d(TAG, "Loading Lantern library") - getOrInitProvider()?.run( - this, - Builder(), + if (Build.VERSION.SDK_INT >= 26) serviceNotification() + Logger.debug(TAG, "Creating VpnBuilder before starting tun2socks") + conn = createBuilder() ?: return + val tunFd = conn.getFd() + Logger.debug(TAG, "Running tun2socks") + Internalsdk.tun2Socks( + tunFd.toLong(), LanternApp.getSession().sOCKS5Addr, LanternApp.getSession().dNSGrabAddr, + VPN_MTU.toLong(), + LanternApp.getSession(), ) - } catch (e: Exception) { - Logger.error(TAG, "Error running VPN", e) + } catch (t: Throwable) { + Logger.e(TAG, "Exception while handling TUN device", t) } finally { - Logger.debug(TAG, "Lantern terminated.") - stop() + Locale.setDefault(defaultLocale) } } - fun stop() { - doStop() - stopSelf() - Logger.d(TAG, "Done stopping") + @Synchronized + private fun stopVpn() { + if (::conn.isInitialized) conn.close() } - private fun doStop() { + override fun killProcesses() { Logger.d(TAG, "stop") try { - Logger.d(TAG, "getting provider") - val provider: Provider? = getOrInitProvider() - Logger.d(TAG, "stopping provider") - provider?.stop() + Logger.d(TAG, "stop") + Internalsdk.stopTun2Socks() + stopVpn() + stopForeground(true) } catch (t: Throwable) { - Logger.e(TAG, "error stopping provider", t) - } - try { - Logger.d(TAG, "updating vpn preference") - LanternApp.getSession().updateVpnPreference(false) - } catch (t: Throwable) { - Logger.e(TAG, "error updating vpn preference", t) - } - } - - @Synchronized fun getOrInitProvider(): Provider? { - Logger.d(TAG, "getOrInitProvider()") - if (provider == null) { - Logger.d(TAG, "Using Go tun2socks") - provider = GoTun2SocksProvider( - getPackageManager(), - LanternApp.getSession().splitTunnelingEnabled(), - HashSet(LanternApp.getSession().appsAllowedAccess()), - ) + Logger.e(TAG, "error stopping tun2socks", t) } - return provider } } diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/vpn/Provider.kt b/android/app/src/main/kotlin/org/getlantern/lantern/vpn/Provider.kt deleted file mode 100644 index 3f0af3cb0..000000000 --- a/android/app/src/main/kotlin/org/getlantern/lantern/vpn/Provider.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.getlantern.lantern.vpn - -import android.net.VpnService - -// A provider provides the implementation of VPN internals. -interface Provider { - @Throws(Exception::class) - fun run(vpnService: VpnService, builder: VpnService.Builder, socksAddr: String, dnsGrabAddr: String) - - @Throws(Exception::class) - fun stop() -} diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/vpn/VpnServiceManager.kt b/android/app/src/main/kotlin/org/getlantern/lantern/vpn/VpnServiceManager.kt new file mode 100644 index 000000000..9fd6b60c5 --- /dev/null +++ b/android/app/src/main/kotlin/org/getlantern/lantern/vpn/VpnServiceManager.kt @@ -0,0 +1,64 @@ +package org.getlantern.lantern.vpn + +import android.content.Context +import android.content.IntentFilter +import io.lantern.model.VpnModel +import kotlinx.coroutines.* +import org.getlantern.lantern.Actions +import org.getlantern.lantern.LanternApp +import org.getlantern.lantern.service.LanternConnection +import org.getlantern.lantern.util.broadcastReceiver +import org.getlantern.mobilesdk.Logger + +class VpnServiceManager( + private val context: Context, + private val vpnModel: VpnModel, +) { + private val vpnServiceConnection = LanternConnection(true) + private val receiver = broadcastReceiver { _, _ -> + Logger.d(TAG, "Received disconnect broadcast") + val manager = LanternApp.notificationManager + manager.cancel(1) + disconnect() + } + + init { + vpnServiceConnection.connect(context) + context.registerReceiver( + receiver, + IntentFilter().apply { + addAction(Actions.DISCONNECT_VPN) + }, + ) + } + + fun connect() { + updateVpnStatus(true) + LanternApp.startService(vpnServiceConnection) + } + + private fun updateVpnStatus(useVpn: Boolean) { + Logger.d(TAG, "Updating VPN status to $useVpn") + LanternApp.getSession().updateVpnPreference(useVpn) + LanternApp.getSession().updateBootUpVpnPreference(useVpn) + vpnModel.setVpnOn(useVpn) + } + + fun onVpnPermissionResult(isGranted: Boolean) { + if (isGranted) connect() + } + + fun destroy() { + context.unregisterReceiver(receiver) + vpnServiceConnection.disconnect(context) + } + + fun disconnect() { + updateVpnStatus(false) + LanternApp.stopService(vpnServiceConnection) + } + + companion object { + private val TAG = VpnServiceManager::class.java.simpleName + } +} diff --git a/internalsdk/tun.go b/internalsdk/tun.go index 65c5204f1..c9c8a986d 100644 --- a/internalsdk/tun.go +++ b/internalsdk/tun.go @@ -29,7 +29,6 @@ var ( // 1. dns packets (any UDP packets to port 53) are routed to dnsGrabAddr // 2. All other udp packets are routed directly to their destination // 3. All TCP traffic is routed through the Lantern proxy at the given socksAddr. -// func Tun2Socks(fd int, socksAddr, dnsGrabAddr string, mtu int, wrappedSession Session) error { runtime.LockOSThread() @@ -38,7 +37,6 @@ func Tun2Socks(fd int, socksAddr, dnsGrabAddr string, mtu int, wrappedSession Se log.Debugf("Starting tun2socks connecting to socks at %v", socksAddr) dev := os.NewFile(uintptr(fd), "tun") - defer dev.Close() socksDialer, err := proxy.SOCKS5("tcp", socksAddr, nil, nil) if err != nil { @@ -90,10 +88,12 @@ func Tun2Socks(fd int, socksAddr, dnsGrabAddr string, mtu int, wrappedSession Se currentIPP = ipp currentDeviceMx.Unlock() - err = ipp.Serve() - if err != io.EOF { - return log.Errorf("unexpected error serving TUN traffic: %v", err) - } + go func() { + err = ipp.Serve() + if err != io.EOF { + log.Errorf("unexpected error serving TUN traffic: %v", err) + } + }() return nil } @@ -108,6 +108,7 @@ func StopTun2Socks() { currentDeviceMx.Lock() ipp := currentIPP + currentDevice.Close() currentDevice = nil currentIPP = nil currentDeviceMx.Unlock()