summaryrefslogtreecommitdiffhomepage
path: root/android/lib
diff options
context:
space:
mode:
Diffstat (limited to 'android/lib')
-rw-r--r--android/lib/common/build.gradle.kts2
-rw-r--r--android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/BuildTypes.kt8
-rw-r--r--android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt43
-rw-r--r--android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorNotificationMessage.kt21
-rw-r--r--android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorStateExtension.kt69
-rw-r--r--android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/Intermittent.kt86
-rw-r--r--android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/JobTracker.kt91
-rw-r--r--android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/LocationConstraintExtensions.kt23
-rw-r--r--android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/StringExtensions.kt29
9 files changed, 372 insertions, 0 deletions
diff --git a/android/lib/common/build.gradle.kts b/android/lib/common/build.gradle.kts
index b472dc5b09..4cc1dfe49c 100644
--- a/android/lib/common/build.gradle.kts
+++ b/android/lib/common/build.gradle.kts
@@ -30,8 +30,10 @@ android {
dependencies {
implementation(project(Dependencies.Mullvad.modelLib))
+ implementation(project(Dependencies.Mullvad.resourceLib))
implementation(project(Dependencies.Mullvad.talpidLib))
+ implementation(Dependencies.jodaTime)
implementation(Dependencies.Kotlin.stdlib)
implementation(Dependencies.KotlinX.coroutinesAndroid)
}
diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/BuildTypes.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/BuildTypes.kt
new file mode 100644
index 0000000000..cfe72339d9
--- /dev/null
+++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/BuildTypes.kt
@@ -0,0 +1,8 @@
+package net.mullvad.mullvadvpn.lib.common.constant
+
+object BuildTypes {
+ const val DEBUG = "debug"
+ const val RELEASE = "release"
+ const val FDROID = "fdroid"
+ const val LEAK_CANARY = "leakCanary"
+}
diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt
new file mode 100644
index 0000000000..b983e3538d
--- /dev/null
+++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt
@@ -0,0 +1,43 @@
+package net.mullvad.mullvadvpn.lib.common.util
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.provider.Settings
+import net.mullvad.mullvadvpn.lib.common.R
+import net.mullvad.mullvadvpn.lib.common.util.SdkUtils.getInstalledPackagesList
+
+private const val ALWAYS_ON_VPN_APP = "always_on_vpn_app"
+
+fun Context.openAccountPageInBrowser(authToken: String) {
+ startActivity(
+ Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.account_url) + "?token=$authToken"))
+ )
+}
+
+fun Context.getAlwaysOnVpnAppName(): String? {
+ return resolveAlwaysOnVpnPackageName()
+ ?.let { currentAlwaysOnVpn ->
+ packageManager.getInstalledPackagesList(0).singleOrNull {
+ it.packageName == currentAlwaysOnVpn && it.packageName != packageName
+ }
+ }
+ ?.applicationInfo
+ ?.loadLabel(packageManager)
+ ?.toString()
+}
+
+// NOTE: This function will return the current Always-on VPN package's name. In case of either
+// Always-on VPN being disabled or not being able to read the state, NULL will be returned.
+fun Context.resolveAlwaysOnVpnPackageName(): String? {
+ return try {
+ Settings.Secure.getString(contentResolver, ALWAYS_ON_VPN_APP)
+ } catch (ex: SecurityException) {
+ null
+ }
+}
+
+fun Context.openLink(uri: Uri) {
+ val intent = Intent(Intent.ACTION_VIEW, uri)
+ startActivity(intent)
+}
diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorNotificationMessage.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorNotificationMessage.kt
new file mode 100644
index 0000000000..f009f4857b
--- /dev/null
+++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorNotificationMessage.kt
@@ -0,0 +1,21 @@
+package net.mullvad.mullvadvpn.lib.common.util
+
+import android.content.res.Resources
+
+data class ErrorNotificationMessage(
+ val titleResourceId: Int,
+ val messageResourceId: Int,
+ val optionalMessageArgument: String? = null
+) {
+ fun getTitleText(resources: Resources): String {
+ return resources.getString(titleResourceId)
+ }
+
+ fun getMessageText(resources: Resources): String {
+ return if (optionalMessageArgument != null) {
+ resources.getString(messageResourceId, optionalMessageArgument)
+ } else {
+ resources.getString(messageResourceId)
+ }
+ }
+}
diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorStateExtension.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorStateExtension.kt
new file mode 100644
index 0000000000..f906ee8f6d
--- /dev/null
+++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorStateExtension.kt
@@ -0,0 +1,69 @@
+package net.mullvad.mullvadvpn.lib.common.util
+
+import android.content.Context
+import net.mullvad.mullvadvpn.lib.common.R
+import net.mullvad.talpid.tunnel.ErrorState
+import net.mullvad.talpid.tunnel.ErrorStateCause
+import net.mullvad.talpid.tunnel.ParameterGenerationError
+import net.mullvad.talpid.util.addressString
+
+fun ErrorState.getErrorNotificationResources(context: Context): ErrorNotificationMessage {
+ return when {
+ cause is ErrorStateCause.InvalidDnsServers -> {
+ ErrorNotificationMessage(
+ R.string.blocking_internet,
+ cause.errorMessageId(),
+ (cause as ErrorStateCause.InvalidDnsServers).addresses.joinToString { address ->
+ address.addressString()
+ }
+ )
+ }
+ cause is ErrorStateCause.VpnPermissionDenied -> {
+ resolveAlwaysOnVpnErrorNotificationMessage(context.getAlwaysOnVpnAppName())
+ }
+ isBlocking -> ErrorNotificationMessage(R.string.blocking_internet, cause.errorMessageId())
+ else -> ErrorNotificationMessage(R.string.critical_error, R.string.failed_to_block_internet)
+ }
+}
+
+private fun resolveAlwaysOnVpnErrorNotificationMessage(
+ alwaysOnVpnAppName: String?
+): ErrorNotificationMessage {
+ return if (alwaysOnVpnAppName != null) {
+ ErrorNotificationMessage(
+ R.string.always_on_vpn_error_notification_title,
+ R.string.always_on_vpn_error_notification_content,
+ alwaysOnVpnAppName
+ )
+ } else {
+ ErrorNotificationMessage(
+ R.string.vpn_permission_error_notification_title,
+ R.string.vpn_permission_error_notification_message
+ )
+ }
+}
+
+fun ErrorStateCause.errorMessageId(): Int {
+ return when (this) {
+ is ErrorStateCause.InvalidDnsServers -> R.string.invalid_dns_servers
+ is ErrorStateCause.AuthFailed -> R.string.auth_failed
+ is ErrorStateCause.Ipv6Unavailable -> R.string.ipv6_unavailable
+ is ErrorStateCause.SetFirewallPolicyError -> R.string.set_firewall_policy_error
+ is ErrorStateCause.SetDnsError -> R.string.set_dns_error
+ is ErrorStateCause.StartTunnelError -> R.string.start_tunnel_error
+ is ErrorStateCause.IsOffline -> R.string.is_offline
+ is ErrorStateCause.TunnelParameterError -> {
+ when (error) {
+ ParameterGenerationError.NoMatchingRelay,
+ ParameterGenerationError.NoMatchingBridgeRelay -> {
+ R.string.no_matching_relay
+ }
+ ParameterGenerationError.NoWireguardKey -> R.string.no_wireguard_key
+ ParameterGenerationError.CustomTunnelHostResultionError -> {
+ R.string.custom_tunnel_host_resolution_error
+ }
+ }
+ }
+ is ErrorStateCause.VpnPermissionDenied -> R.string.vpn_permission_denied_error
+ }
+}
diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/Intermittent.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/Intermittent.kt
new file mode 100644
index 0000000000..38b152b00a
--- /dev/null
+++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/Intermittent.kt
@@ -0,0 +1,86 @@
+package net.mullvad.mullvadvpn.lib.common.util
+
+import kotlin.properties.Delegates.observable
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.Semaphore
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.sync.withPermit
+import net.mullvad.talpid.util.EventNotifier
+
+// Wrapper to allow awaiting for intermittent values.
+//
+// Wraps a property that is changed from time to time and that can become unavailable (null). This
+// behaves in a way similar to `CompletableDeferred`, but the value can be set and reset multiple
+// times.
+//
+// Calling `await` will either provide the value if it's available, or suspend until it becomes
+// available and then return it.
+//
+// Calling `update` will set the internal value after it guarantees that no other coroutine is
+// currently reading the value (through a permit from the semaphore). After the value is set, it
+// provides a permit to the semaphore so that suspended coroutines can use the new value.
+//
+// Extra initialization can be done on the intermittent value when it becomes available and before
+// it is provided to the awaiting coroutines, through the use of listener callbacks. These are
+// called after the value is updated but before it is made available to the coroutines.
+class Intermittent<T> {
+ private val notifier = EventNotifier<T?>(null)
+ private val semaphore = Semaphore(1, 1)
+ private val writeLock = Mutex()
+
+ private var updateJob: Job? = null
+ private var value by notifier.notifiable()
+
+ // When the internal value is updated, listeners can be notified before the awaiting coroutines
+ // resume execution. This allows performing any extra initialization before the value is made
+ // available for usage.
+ fun registerListener(id: Any, listener: (T?) -> Unit) = notifier.subscribe(id, listener)
+ fun unregisterListener(id: Any) = notifier.unsubscribe(id)
+
+ suspend fun await(): T {
+ return semaphore.withPermit { value!! }
+ }
+
+ suspend fun update(newValue: T?) {
+ writeLock.withLock {
+ if (newValue != value) {
+ if (value != null) {
+ semaphore.acquire()
+ }
+
+ // This will trigger the listeners to run before the awaiting coroutines resume
+ value = newValue
+
+ if (newValue != null) {
+ semaphore.release()
+ }
+ }
+ }
+ }
+
+ // Helper method that spawns a coroutine to update the value.
+ fun spawnUpdate(newValue: T?) {
+ synchronized(this@Intermittent) {
+ val previousUpdate = updateJob
+
+ updateJob =
+ GlobalScope.launch(Dispatchers.Default) {
+ previousUpdate?.join()
+ update(newValue)
+ }
+ }
+ }
+
+ // Helper method that provides a simple way to change the wrapped value.
+ // The method returns a property delegate that will spawn a coroutine to update the wrapped
+ // value every time the property is written to.
+ fun source() = observable<T?>(null) { _, _, newValue -> spawnUpdate(newValue) }
+
+ fun onDestroy() {
+ notifier.unsubscribeAll()
+ }
+}
diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/JobTracker.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/JobTracker.kt
new file mode 100644
index 0000000000..edb76ed4ae
--- /dev/null
+++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/JobTracker.kt
@@ -0,0 +1,91 @@
+package net.mullvad.mullvadvpn.lib.common.util
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+
+class JobTracker {
+ private val jobs = HashMap<Long, Job>()
+ private val reaperJobs = HashMap<Long, Job>()
+ private val namedJobs = HashMap<String, Long>()
+
+ private var jobIdCounter = 0L
+
+ fun newJob(job: Job): Long {
+ synchronized(jobs) {
+ val jobId = jobIdCounter
+
+ jobIdCounter += 1
+
+ jobs.put(jobId, job)
+
+ reaperJobs.put(
+ jobId,
+ GlobalScope.launch(Dispatchers.Default) {
+ job.join()
+
+ synchronized(jobs) { jobs.remove(jobId) }
+ }
+ )
+
+ return jobId
+ }
+ }
+
+ fun newJob(name: String, job: Job): Long {
+ synchronized(namedJobs) {
+ cancelJob(name)
+
+ val newJobId = newJob(job)
+
+ namedJobs.put(name, newJobId)
+
+ return newJobId
+ }
+ }
+
+ fun newBackgroundJob(name: String, jobBody: suspend () -> Unit): Long {
+ return newJob(name, GlobalScope.launch(Dispatchers.Default) { jobBody() })
+ }
+
+ fun newUiJob(name: String, jobBody: suspend () -> Unit): Long {
+ return newJob(name, GlobalScope.launch(Dispatchers.Main) { jobBody() })
+ }
+
+ suspend fun <T> runOnBackground(jobBody: suspend () -> T): T {
+ val job = GlobalScope.async(Dispatchers.Default) { jobBody() }
+
+ newJob(job)
+
+ return job.await()
+ }
+
+ fun cancelJob(name: String) {
+ synchronized(namedJobs) { namedJobs.remove(name)?.let { oldJobId -> cancelJob(oldJobId) } }
+ }
+
+ fun cancelJob(jobId: Long) {
+ synchronized(jobs) {
+ jobs.remove(jobId)?.cancel()
+ reaperJobs.remove(jobId)?.cancel()
+ }
+ }
+
+ fun cancelAllJobs() {
+ synchronized(jobs) {
+ for (job in jobs.values) {
+ job.cancel()
+ }
+
+ for (job in reaperJobs.values) {
+ job.cancel()
+ }
+
+ jobs.clear()
+ reaperJobs.clear()
+ namedJobs.clear()
+ }
+ }
+}
diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/LocationConstraintExtensions.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/LocationConstraintExtensions.kt
new file mode 100644
index 0000000000..d845e3aba9
--- /dev/null
+++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/LocationConstraintExtensions.kt
@@ -0,0 +1,23 @@
+package net.mullvad.mullvadvpn.lib.common.util
+
+import net.mullvad.mullvadvpn.model.Constraint
+import net.mullvad.mullvadvpn.model.GeographicLocationConstraint
+import net.mullvad.mullvadvpn.model.LocationConstraint
+
+fun LocationConstraint.toGeographicLocationConstraint(): GeographicLocationConstraint? =
+ when (this) {
+ is LocationConstraint.Location -> this.location
+ is LocationConstraint.CustomList -> null
+ }
+
+fun Constraint<LocationConstraint>.toGeographicLocationConstraint():
+ Constraint<GeographicLocationConstraint> =
+ when (this) {
+ is Constraint.Only ->
+ when (value) {
+ is LocationConstraint.Location ->
+ Constraint.Only((value as LocationConstraint.Location).location)
+ is LocationConstraint.CustomList -> Constraint.Any()
+ }
+ is Constraint.Any -> Constraint.Any()
+ }
diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/StringExtensions.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/StringExtensions.kt
new file mode 100644
index 0000000000..934ba1d635
--- /dev/null
+++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/StringExtensions.kt
@@ -0,0 +1,29 @@
+package net.mullvad.mullvadvpn.lib.common.util
+
+import net.mullvad.mullvadvpn.lib.common.BuildConfig
+import net.mullvad.mullvadvpn.lib.common.constant.BuildTypes
+import org.joda.time.DateTime
+import org.joda.time.format.DateTimeFormat
+
+private const val EXPIRY_FORMAT = "YYYY-MM-dd HH:mm:ss z"
+
+fun String.capitalizeFirstCharOfEachWord(): String {
+ return split(" ")
+ .joinToString(" ") { word -> word.replaceFirstChar { firstChar -> firstChar.uppercase() } }
+ .trimEnd()
+}
+
+fun String.parseAsDateTime(): DateTime? {
+ return try {
+ DateTime.parse(this, DateTimeFormat.forPattern(EXPIRY_FORMAT))
+ } catch (ex: Exception) {
+ null
+ }
+}
+
+fun String.appendHideNavOnReleaseBuild(): String =
+ if (BuildTypes.RELEASE == BuildConfig.BUILD_TYPE) {
+ "$this?hide_nav"
+ } else {
+ this
+ }