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()