From 53f4a9ce311d612d43fa770cf7e8f8e98fbb43a0 Mon Sep 17 00:00:00 2001
From: Guillaume Jacquart <guillaume.jacquart@hoodbrains.com>
Date: Tue, 12 Sep 2023 06:17:39 +0000
Subject: 2: organise module with clean archi, use Koin for injection.

---
 ipscrambling/src/main/AndroidManifest.xml          |   2 +-
 .../ipscrambler/IpScramblerModule.kt               | 308 +++++++++++++++++++++
 .../e/advancedprivacy/ipscrambler/KoinModule.kt    |   8 +
 .../ipscrambler/IIpScramblerModule.kt              |  54 ----
 .../ipscrambler/IpScramblerModule.kt               | 301 --------------------
 5 files changed, 317 insertions(+), 356 deletions(-)
 create mode 100644 ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/IpScramblerModule.kt
 create mode 100644 ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/KoinModule.kt
 delete mode 100644 ipscrambling/src/main/java/foundation/e/privacymodules/ipscrambler/IIpScramblerModule.kt
 delete mode 100644 ipscrambling/src/main/java/foundation/e/privacymodules/ipscrambler/IpScramblerModule.kt

(limited to 'ipscrambling/src')

diff --git a/ipscrambling/src/main/AndroidManifest.xml b/ipscrambling/src/main/AndroidManifest.xml
index e948147..d6496f0 100644
--- a/ipscrambling/src/main/AndroidManifest.xml
+++ b/ipscrambling/src/main/AndroidManifest.xml
@@ -1,5 +1,5 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="foundation.e.privacymodules.tor"
+    package="foundation.e.advancedprivacy.ipscrambler"
     android:installLocation="internalOnly"
     >
 
diff --git a/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/IpScramblerModule.kt b/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/IpScramblerModule.kt
new file mode 100644
index 0000000..d1f01a0
--- /dev/null
+++ b/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/IpScramblerModule.kt
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2021 E FOUNDATION
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.advancedprivacy.ipscrambler
+
+import android.annotation.SuppressLint
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.VpnService
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.os.Message
+import android.util.Log
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import org.torproject.android.service.OrbotConstants
+import org.torproject.android.service.OrbotConstants.ACTION_STOP_FOREGROUND_TASK
+import org.torproject.android.service.OrbotService
+import org.torproject.android.service.util.Prefs
+import java.security.InvalidParameterException
+
+@SuppressLint("CommitPrefEdits")
+class IpScramblerModule(private val context: Context) {
+    interface Listener {
+        fun onStatusChanged(newStatus: Status)
+        fun log(message: String)
+        fun onTrafficUpdate(upload: Long, download: Long, read: Long, write: Long)
+    }
+
+    enum class Status {
+        OFF, ON, STARTING, STOPPING, START_DISABLED
+    }
+    companion object {
+        const val TAG = "IpScramblerModule"
+
+        private val EXIT_COUNTRY_CODES = setOf("DE", "AT", "SE", "CH", "IS", "CA", "US", "ES", "FR", "BG", "PL", "AU", "BR", "CZ", "DK", "FI", "GB", "HU", "NL", "JP", "RO", "RU", "SG", "SK")
+
+        // Key where exit country is stored by orbot service.
+        private const val PREFS_KEY_EXIT_NODES = "pref_exit_nodes"
+        // Copy of the package private OrbotService.NOTIFY_ID value.
+        // const val ORBOT_SERVICE_NOTIFY_ID_COPY = 1
+    }
+
+    private var currentStatus: Status? = null
+    private val listeners = mutableSetOf<Listener>()
+
+    private val localBroadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context, intent: Intent) {
+            val action = intent.action ?: return
+            if (action == OrbotConstants.ACTION_RUNNING_SYNC) {
+                try {
+                    intent.getStringExtra(OrbotConstants.EXTRA_STATUS)?.let {
+                        val newStatus = Status.valueOf(it)
+                        currentStatus = newStatus
+                    }
+                } catch (e: Exception) {
+                    Log.e(TAG, "Can't parse Orbot service status.")
+                }
+                return
+            }
+
+            val msg = messageHandler.obtainMessage()
+            msg.obj = action
+            msg.data = intent.extras
+            messageHandler.sendMessage(msg)
+        }
+    }
+
+    private val messageHandler: Handler = object : Handler(Looper.getMainLooper()) {
+        override fun handleMessage(msg: Message) {
+            val action = msg.obj as? String ?: return
+            val data = msg.data
+            when (action) {
+                OrbotConstants.LOCAL_ACTION_LOG ->
+                    data.getString(OrbotConstants.LOCAL_EXTRA_LOG)?.let { newLog(it) }
+
+                OrbotConstants.LOCAL_ACTION_BANDWIDTH -> {
+                    trafficUpdate(
+                        data.getLong("up", 0),
+                        data.getLong("down", 0),
+                        data.getLong("written", 0),
+                        data.getLong("read", 0)
+                    )
+                }
+
+                OrbotConstants.LOCAL_ACTION_PORTS -> {
+                    httpProxyPort = data.getInt(OrbotService.EXTRA_HTTP_PROXY_PORT, -1)
+                    socksProxyPort = data.getInt(OrbotService.EXTRA_SOCKS_PROXY_PORT, -1)
+                }
+
+                OrbotConstants.LOCAL_ACTION_STATUS ->
+                    data.getString(OrbotConstants.EXTRA_STATUS)?.let {
+                        try {
+                            val newStatus = Status.valueOf(it)
+                            updateStatus(newStatus, force = true)
+                        } catch (e: Exception) {
+                            Log.e(TAG, "Can't parse Orbot service status.")
+                        }
+                    }
+            }
+            super.handleMessage(msg)
+        }
+    }
+
+    init {
+        Prefs.setContext(context)
+
+        val lbm = LocalBroadcastManager.getInstance(context)
+        lbm.registerReceiver(
+            localBroadcastReceiver,
+            IntentFilter(OrbotConstants.LOCAL_ACTION_STATUS)
+        )
+        lbm.registerReceiver(
+            localBroadcastReceiver,
+            IntentFilter(OrbotConstants.LOCAL_ACTION_BANDWIDTH)
+        )
+        lbm.registerReceiver(
+            localBroadcastReceiver,
+            IntentFilter(OrbotConstants.LOCAL_ACTION_LOG)
+        )
+        lbm.registerReceiver(
+            localBroadcastReceiver,
+            IntentFilter(OrbotConstants.LOCAL_ACTION_PORTS)
+        )
+        lbm.registerReceiver(
+            localBroadcastReceiver,
+            IntentFilter(OrbotConstants.ACTION_RUNNING_SYNC)
+        )
+
+        Prefs.getSharedPrefs(context).edit()
+            .putInt(OrbotConstants.PREFS_DNS_PORT, OrbotConstants.TOR_DNS_PORT_DEFAULT)
+            .apply()
+    }
+
+    private fun updateStatus(status: Status, force: Boolean = false) {
+        if (force || status != currentStatus) {
+            currentStatus = status
+            listeners.forEach {
+                it.onStatusChanged(status)
+            }
+        }
+    }
+
+    private fun isServiceRunning(): Boolean {
+        // Reset status, and then ask to refresh it synchronously.
+        currentStatus = Status.OFF
+        LocalBroadcastManager.getInstance(context)
+            .sendBroadcastSync(Intent(OrbotConstants.ACTION_CHECK_RUNNING_SYNC))
+        return currentStatus != Status.OFF
+    }
+
+    private fun newLog(message: String) {
+        listeners.forEach { it.log(message) }
+    }
+
+    private fun trafficUpdate(upload: Long, download: Long, read: Long, write: Long) {
+        listeners.forEach { it.onTrafficUpdate(upload, download, read, write) }
+    }
+
+    private fun sendIntentToService(action: String, extra: Bundle? = null) {
+        val intent = Intent(context, OrbotService::class.java)
+        intent.action = action
+        extra?.let { intent.putExtras(it) }
+        context.startService(intent)
+    }
+
+    @SuppressLint("ApplySharedPref")
+    private fun saveTorifiedApps(packageNames: Collection<String>) {
+        packageNames.joinToString("|")
+        Prefs.getSharedPrefs(context).edit().putString(
+            OrbotConstants.PREFS_KEY_TORIFIED, packageNames.joinToString("|")
+        ).commit()
+
+        if (isServiceRunning()) {
+            sendIntentToService(OrbotConstants.ACTION_RESTART_VPN)
+        }
+    }
+
+    private fun getTorifiedApps(): Set<String> {
+        val list = Prefs.getSharedPrefs(context).getString(OrbotConstants.PREFS_KEY_TORIFIED, "")
+            ?.split("|")
+        return if (list == null || list == listOf("")) {
+            emptySet()
+        } else {
+            list.toSet()
+        }
+    }
+
+    @SuppressLint("ApplySharedPref")
+    private fun setExitCountryCode(countryCode: String) {
+        val countryParam = when {
+            countryCode.isEmpty() -> ""
+            countryCode in EXIT_COUNTRY_CODES -> "{$countryCode}"
+            else -> throw InvalidParameterException(
+                "Only these countries are available: ${EXIT_COUNTRY_CODES.joinToString { ", " } }"
+            )
+        }
+
+        if (isServiceRunning()) {
+            val extra = Bundle()
+            extra.putString("exit", countryParam)
+            sendIntentToService(OrbotConstants.CMD_SET_EXIT, extra)
+        } else {
+            Prefs.getSharedPrefs(context)
+                .edit().putString(PREFS_KEY_EXIT_NODES, countryParam)
+                .commit()
+        }
+    }
+
+    private fun getExitCountryCode(): String {
+        val raw = Prefs.getExitNodes()
+        return if (raw.isEmpty()) raw else raw.slice(1..2)
+    }
+
+    fun prepareAndroidVpn(): Intent? {
+        return VpnService.prepare(context)
+    }
+
+    fun start(enableNotification: Boolean) {
+        Prefs.enableNotification(enableNotification)
+        Prefs.putUseVpn(true)
+        Prefs.putStartOnBoot(true)
+
+        sendIntentToService(OrbotConstants.ACTION_START)
+        sendIntentToService(OrbotConstants.ACTION_START_VPN)
+    }
+
+    fun stop() {
+        updateStatus(Status.STOPPING)
+
+        Prefs.putUseVpn(false)
+        Prefs.putStartOnBoot(false)
+
+        sendIntentToService(OrbotConstants.ACTION_STOP_VPN)
+        sendIntentToService(
+            action = OrbotConstants.ACTION_STOP,
+            extra = Bundle().apply { putBoolean(ACTION_STOP_FOREGROUND_TASK, true) }
+        )
+        stoppingWatchdog(5)
+    }
+
+    private fun stoppingWatchdog(countDown: Int) {
+        Handler(Looper.getMainLooper()).postDelayed(
+            {
+                if (isServiceRunning() && countDown > 0) {
+                    stoppingWatchdog(countDown - 1)
+                } else {
+                    updateStatus(Status.OFF, force = true)
+                }
+            },
+            500
+        )
+    }
+
+    fun requestStatus() {
+        if (isServiceRunning()) {
+            sendIntentToService(OrbotConstants.ACTION_STATUS)
+        } else {
+            updateStatus(Status.OFF, force = true)
+        }
+    }
+
+    var appList: Set<String>
+        get() = getTorifiedApps()
+        set(value) = saveTorifiedApps(value)
+
+    var exitCountry: String
+        get() = getExitCountryCode()
+        set(value) = setExitCountryCode(value)
+
+    fun getAvailablesLocations(): Set<String> = EXIT_COUNTRY_CODES
+
+    var httpProxyPort: Int = -1
+        private set
+
+    var socksProxyPort: Int = -1
+        private set
+
+    fun addListener(listener: Listener) {
+        listeners.add(listener)
+    }
+    fun removeListener(listener: Listener) {
+        listeners.remove(listener)
+    }
+    fun clearListeners() {
+        listeners.clear()
+    }
+
+    fun onCleared() {
+        LocalBroadcastManager.getInstance(context).unregisterReceiver(localBroadcastReceiver)
+    }
+}
diff --git a/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/KoinModule.kt b/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/KoinModule.kt
new file mode 100644
index 0000000..4f80ef4
--- /dev/null
+++ b/ipscrambling/src/main/java/foundation/e/advancedprivacy/ipscrambler/KoinModule.kt
@@ -0,0 +1,8 @@
+package foundation.e.advancedprivacy.ipscrambler
+
+import org.koin.core.module.dsl.singleOf
+import org.koin.dsl.module
+
+val ipScramblerModule = module {
+    singleOf(::IpScramblerModule)
+}
diff --git a/ipscrambling/src/main/java/foundation/e/privacymodules/ipscrambler/IIpScramblerModule.kt b/ipscrambling/src/main/java/foundation/e/privacymodules/ipscrambler/IIpScramblerModule.kt
deleted file mode 100644
index 859319a..0000000
--- a/ipscrambling/src/main/java/foundation/e/privacymodules/ipscrambler/IIpScramblerModule.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2021 E FOUNDATION
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-
-package foundation.e.privacymodules.ipscrambler
-
-import android.content.Intent
-
-interface IIpScramblerModule {
-    fun prepareAndroidVpn(): Intent?
-
-    fun start(enableNotification: Boolean = true)
-
-    fun stop()
-
-    fun requestStatus()
-
-    var appList: Set<String>
-
-    var exitCountry: String
-    fun getAvailablesLocations(): Set<String>
-
-    val httpProxyPort: Int
-    val socksProxyPort: Int
-
-    fun addListener(listener: Listener)
-    fun removeListener(listener: Listener)
-    fun clearListeners()
-
-    fun onCleared()
-
-    interface Listener {
-        fun onStatusChanged(newStatus: Status)
-        fun log(message: String)
-        fun onTrafficUpdate(upload: Long, download: Long, read: Long, write: Long)
-    }
-
-    enum class Status {
-        OFF, ON, STARTING, STOPPING, START_DISABLED
-    }
-}
diff --git a/ipscrambling/src/main/java/foundation/e/privacymodules/ipscrambler/IpScramblerModule.kt b/ipscrambling/src/main/java/foundation/e/privacymodules/ipscrambler/IpScramblerModule.kt
deleted file mode 100644
index 1c39330..0000000
--- a/ipscrambling/src/main/java/foundation/e/privacymodules/ipscrambler/IpScramblerModule.kt
+++ /dev/null
@@ -1,301 +0,0 @@
-/*
- * Copyright (C) 2021 E FOUNDATION
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-
-package foundation.e.privacymodules.ipscrambler
-
-import android.annotation.SuppressLint
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.net.VpnService
-import android.os.Bundle
-import android.os.Handler
-import android.os.Looper
-import android.os.Message
-import android.util.Log
-import androidx.localbroadcastmanager.content.LocalBroadcastManager
-import foundation.e.privacymodules.ipscrambler.IIpScramblerModule.Listener
-import foundation.e.privacymodules.ipscrambler.IIpScramblerModule.Status
-import org.torproject.android.service.OrbotConstants
-import org.torproject.android.service.OrbotConstants.ACTION_STOP_FOREGROUND_TASK
-import org.torproject.android.service.OrbotService
-import org.torproject.android.service.util.Prefs
-import java.security.InvalidParameterException
-
-@SuppressLint("CommitPrefEdits")
-class IpScramblerModule(private val context: Context) : IIpScramblerModule {
-    companion object {
-        const val TAG = "IpScramblerModule"
-
-        private val EXIT_COUNTRY_CODES = setOf("DE", "AT", "SE", "CH", "IS", "CA", "US", "ES", "FR", "BG", "PL", "AU", "BR", "CZ", "DK", "FI", "GB", "HU", "NL", "JP", "RO", "RU", "SG", "SK")
-
-        // Key where exit country is stored by orbot service.
-        private const val PREFS_KEY_EXIT_NODES = "pref_exit_nodes"
-        // Copy of the package private OrbotService.NOTIFY_ID value.
-        // const val ORBOT_SERVICE_NOTIFY_ID_COPY = 1
-    }
-
-    private var currentStatus: Status? = null
-    private val listeners = mutableSetOf<Listener>()
-
-    private val localBroadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() {
-        override fun onReceive(context: Context, intent: Intent) {
-            val action = intent.action ?: return
-            if (action == OrbotConstants.ACTION_RUNNING_SYNC) {
-                try {
-                    intent.getStringExtra(OrbotConstants.EXTRA_STATUS)?.let {
-                        val newStatus = Status.valueOf(it)
-                        currentStatus = newStatus
-                    }
-                } catch (e: Exception) {
-                    Log.e(TAG, "Can't parse Orbot service status.")
-                }
-                return
-            }
-
-            val msg = messageHandler.obtainMessage()
-            msg.obj = action
-            msg.data = intent.extras
-            messageHandler.sendMessage(msg)
-        }
-    }
-
-    private val messageHandler: Handler = object : Handler(Looper.getMainLooper()) {
-        override fun handleMessage(msg: Message) {
-            val action = msg.obj as? String ?: return
-            val data = msg.data
-            when (action) {
-                OrbotConstants.LOCAL_ACTION_LOG ->
-                    data.getString(OrbotConstants.LOCAL_EXTRA_LOG)?.let { newLog(it) }
-
-                OrbotConstants.LOCAL_ACTION_BANDWIDTH -> {
-                    trafficUpdate(
-                        data.getLong("up", 0),
-                        data.getLong("down", 0),
-                        data.getLong("written", 0),
-                        data.getLong("read", 0)
-                    )
-                }
-
-                OrbotConstants.LOCAL_ACTION_PORTS -> {
-                    httpProxyPort = data.getInt(OrbotService.EXTRA_HTTP_PROXY_PORT, -1)
-                    socksProxyPort = data.getInt(OrbotService.EXTRA_SOCKS_PROXY_PORT, -1)
-                }
-
-                OrbotConstants.LOCAL_ACTION_STATUS ->
-                    data.getString(OrbotConstants.EXTRA_STATUS)?.let {
-                        try {
-                            val newStatus = Status.valueOf(it)
-                            updateStatus(newStatus, force = true)
-                        } catch (e: Exception) {
-                            Log.e(TAG, "Can't parse Orbot service status.")
-                        }
-                    }
-            }
-            super.handleMessage(msg)
-        }
-    }
-
-    init {
-        Prefs.setContext(context)
-
-        val lbm = LocalBroadcastManager.getInstance(context)
-        lbm.registerReceiver(
-            localBroadcastReceiver,
-            IntentFilter(OrbotConstants.LOCAL_ACTION_STATUS)
-        )
-        lbm.registerReceiver(
-            localBroadcastReceiver,
-            IntentFilter(OrbotConstants.LOCAL_ACTION_BANDWIDTH)
-        )
-        lbm.registerReceiver(
-            localBroadcastReceiver,
-            IntentFilter(OrbotConstants.LOCAL_ACTION_LOG)
-        )
-        lbm.registerReceiver(
-            localBroadcastReceiver,
-            IntentFilter(OrbotConstants.LOCAL_ACTION_PORTS)
-        )
-        lbm.registerReceiver(
-            localBroadcastReceiver,
-            IntentFilter(OrbotConstants.ACTION_RUNNING_SYNC)
-        )
-
-        Prefs.getSharedPrefs(context).edit()
-            .putInt(OrbotConstants.PREFS_DNS_PORT, OrbotConstants.TOR_DNS_PORT_DEFAULT)
-            .apply()
-    }
-
-    private fun updateStatus(status: Status, force: Boolean = false) {
-        if (force || status != currentStatus) {
-            currentStatus = status
-            listeners.forEach {
-                it.onStatusChanged(status)
-            }
-        }
-    }
-
-    private fun isServiceRunning(): Boolean {
-        // Reset status, and then ask to refresh it synchronously.
-        currentStatus = Status.OFF
-        LocalBroadcastManager.getInstance(context)
-            .sendBroadcastSync(Intent(OrbotConstants.ACTION_CHECK_RUNNING_SYNC))
-        return currentStatus != Status.OFF
-    }
-
-    private fun newLog(message: String) {
-        listeners.forEach { it.log(message) }
-    }
-
-    private fun trafficUpdate(upload: Long, download: Long, read: Long, write: Long) {
-        listeners.forEach { it.onTrafficUpdate(upload, download, read, write) }
-    }
-
-    private fun sendIntentToService(action: String, extra: Bundle? = null) {
-        val intent = Intent(context, OrbotService::class.java)
-        intent.action = action
-        extra?.let { intent.putExtras(it) }
-        context.startService(intent)
-    }
-
-    @SuppressLint("ApplySharedPref")
-    private fun saveTorifiedApps(packageNames: Collection<String>) {
-        packageNames.joinToString("|")
-        Prefs.getSharedPrefs(context).edit().putString(
-            OrbotConstants.PREFS_KEY_TORIFIED, packageNames.joinToString("|")
-        ).commit()
-
-        if (isServiceRunning()) {
-            sendIntentToService(OrbotConstants.ACTION_RESTART_VPN)
-        }
-    }
-
-    private fun getTorifiedApps(): Set<String> {
-        val list = Prefs.getSharedPrefs(context).getString(OrbotConstants.PREFS_KEY_TORIFIED, "")
-            ?.split("|")
-        return if (list == null || list == listOf("")) {
-            emptySet()
-        } else {
-            list.toSet()
-        }
-    }
-
-    @SuppressLint("ApplySharedPref")
-    private fun setExitCountryCode(countryCode: String) {
-        val countryParam = when {
-            countryCode.isEmpty() -> ""
-            countryCode in EXIT_COUNTRY_CODES -> "{$countryCode}"
-            else -> throw InvalidParameterException(
-                "Only these countries are available: ${EXIT_COUNTRY_CODES.joinToString { ", " } }"
-            )
-        }
-
-        if (isServiceRunning()) {
-            val extra = Bundle()
-            extra.putString("exit", countryParam)
-            sendIntentToService(OrbotConstants.CMD_SET_EXIT, extra)
-        } else {
-            Prefs.getSharedPrefs(context)
-                .edit().putString(PREFS_KEY_EXIT_NODES, countryParam)
-                .commit()
-        }
-    }
-
-    private fun getExitCountryCode(): String {
-        val raw = Prefs.getExitNodes()
-        return if (raw.isEmpty()) raw else raw.slice(1..2)
-    }
-
-    override fun prepareAndroidVpn(): Intent? {
-        return VpnService.prepare(context)
-    }
-
-    override fun start(enableNotification: Boolean) {
-        Prefs.enableNotification(enableNotification)
-        Prefs.putUseVpn(true)
-        Prefs.putStartOnBoot(true)
-
-        sendIntentToService(OrbotConstants.ACTION_START)
-        sendIntentToService(OrbotConstants.ACTION_START_VPN)
-    }
-
-    override fun stop() {
-        updateStatus(Status.STOPPING)
-
-        Prefs.putUseVpn(false)
-        Prefs.putStartOnBoot(false)
-
-        sendIntentToService(OrbotConstants.ACTION_STOP_VPN)
-        sendIntentToService(
-            action = OrbotConstants.ACTION_STOP,
-            extra = Bundle().apply { putBoolean(ACTION_STOP_FOREGROUND_TASK, true) }
-        )
-        stoppingWatchdog(5)
-    }
-
-    private fun stoppingWatchdog(countDown: Int) {
-        Handler(Looper.getMainLooper()).postDelayed(
-            {
-                if (isServiceRunning() && countDown > 0) {
-                    stoppingWatchdog(countDown - 1)
-                } else {
-                    updateStatus(Status.OFF, force = true)
-                }
-            },
-            500
-        )
-    }
-
-    override fun requestStatus() {
-        if (isServiceRunning()) {
-            sendIntentToService(OrbotConstants.ACTION_STATUS)
-        } else {
-            updateStatus(Status.OFF, force = true)
-        }
-    }
-
-    override var appList: Set<String>
-        get() = getTorifiedApps()
-        set(value) = saveTorifiedApps(value)
-
-    override var exitCountry: String
-        get() = getExitCountryCode()
-        set(value) = setExitCountryCode(value)
-
-    override fun getAvailablesLocations(): Set<String> = EXIT_COUNTRY_CODES
-
-    override var httpProxyPort: Int = -1
-        private set
-
-    override var socksProxyPort: Int = -1
-        private set
-
-    override fun addListener(listener: Listener) {
-        listeners.add(listener)
-    }
-    override fun removeListener(listener: Listener) {
-        listeners.remove(listener)
-    }
-    override fun clearListeners() {
-        listeners.clear()
-    }
-
-    override fun onCleared() {
-        LocalBroadcastManager.getInstance(context).unregisterReceiver(localBroadcastReceiver)
-    }
-}
-- 
cgit v1.2.1