diff options
| author | Albin <albin@mullvad.net> | 2023-07-28 09:45:43 +0200 |
|---|---|---|
| committer | Albin <albin@mullvad.net> | 2023-07-28 10:45:57 +0200 |
| commit | c554feeb10567ae42a1f7b483527491832a6dba2 (patch) | |
| tree | 8b10e3587652eb42db4540ede704e9037e3bef5f /android/lib | |
| parent | e5689f041f3823e6d06a1b8bebb615427837d7da (diff) | |
| download | mullvadvpn-c554feeb10567ae42a1f7b483527491832a6dba2.tar.xz mullvadvpn-c554feeb10567ae42a1f7b483527491832a6dba2.zip | |
Move common utils to common module
Diffstat (limited to 'android/lib')
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 + } |
