diff options
| author | Albin <albin@mullvad.net> | 2026-04-22 12:51:48 +0200 |
|---|---|---|
| committer | Albin <albin@mullvad.net> | 2026-04-22 12:51:48 +0200 |
| commit | ef6b61a78c2f8461578c9dcef9592b4580eb9ff9 (patch) | |
| tree | 8e487acfe1b1d309ec587385b05c27e0a41fc19c | |
| parent | 9e9af2d3e7d0cb497e131e43e16229091ff8cda2 (diff) | |
| parent | 05e77fabf590e0db31f5992817b942a8ef8485b4 (diff) | |
| download | mullvadvpn-ef6b61a78c2f8461578c9dcef9592b4580eb9ff9.tar.xz mullvadvpn-ef6b61a78c2f8461578c9dcef9592b4580eb9ff9.zip | |
Merge branch 'determine-download-link-based-on-install-source-droid-2623'
17 files changed, 201 insertions, 45 deletions
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 023508752f..fa94ff9f0b 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -393,6 +393,8 @@ dependencies { implementation(projects.lib.feature.appicon.impl) implementation(projects.lib.feature.appinfo.impl) implementation(projects.lib.feature.appinfo.api) + implementation(projects.lib.feature.applisting.impl) + implementation(projects.lib.feature.applisting.api) implementation(projects.lib.feature.appearance.impl) implementation(projects.lib.feature.autoconnect.impl) implementation(projects.lib.feature.customlist.impl) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt index c86532f65f..686e322d74 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt @@ -20,6 +20,10 @@ import net.mullvad.mullvadvpn.feature.apiaccess.impl.screen.save.SaveApiAccessMe import net.mullvad.mullvadvpn.feature.appicon.impl.AppIconViewModel import net.mullvad.mullvadvpn.feature.appinfo.impl.AppInfoViewModel import net.mullvad.mullvadvpn.feature.appinfo.impl.changelog.ChangelogViewModel +import net.mullvad.mullvadvpn.feature.applisting.api.ResolveAppListingUseCase +import net.mullvad.mullvadvpn.feature.applisting.impl.AndroidInstallSourceProvider +import net.mullvad.mullvadvpn.feature.applisting.impl.InstallSourceProvider +import net.mullvad.mullvadvpn.feature.applisting.impl.ResolveAppListingUseCaseImpl import net.mullvad.mullvadvpn.feature.autoconnect.impl.AutoConnectAndLockdownModeViewModel import net.mullvad.mullvadvpn.feature.customlist.impl.screen.create.CreateCustomListDialogViewModel import net.mullvad.mullvadvpn.feature.customlist.impl.screen.delete.DeleteCustomListConfirmationViewModel @@ -129,7 +133,15 @@ val uiModule = module { ComponentName(androidContext(), AutoStartVpnBootCompletedReceiver::class.java) } - single { PackageName(androidContext().packageName) } + single<InstallSourceProvider> { AndroidInstallSourceProvider(androidContext()) } + single<ResolveAppListingUseCase> { + ResolveAppListingUseCaseImpl( + resources = androidContext().resources, + packageName = PackageName(androidContext().packageName), + isPlayBuild = IS_PLAY_BUILD, + installSourceProvider = get(), + ) + } single { ApplicationsProvider(get(), get()) } scope<MainActivity> { scoped { ServiceConnectionManager(androidContext()) } } single { InetAddressValidator.getInstance() } @@ -263,10 +275,8 @@ val uiModule = module { viewModel { AppInfoViewModel( appVersionInfoRepository = get(), - resources = get(), isPlayBuild = IS_PLAY_BUILD, - isFdroidBuild = false, - self = get(), + resolveAppListing = get(), ) } viewModel { @@ -283,10 +293,8 @@ val uiModule = module { connectionProxy = get(), lastKnownLocationUseCase = get(), systemVpnSettingsUseCase = get(), - resources = get(), isPlayBuild = IS_PLAY_BUILD, - isFdroidBuild = false, - self = get(), + resolveAppListing = get(), ) } viewModel { params -> DeviceListViewModel(accountNumber = params.get(), get()) } diff --git a/android/lib/feature/appinfo/impl/build.gradle.kts b/android/lib/feature/appinfo/impl/build.gradle.kts index ea0ec035e7..e408702233 100644 --- a/android/lib/feature/appinfo/impl/build.gradle.kts +++ b/android/lib/feature/appinfo/impl/build.gradle.kts @@ -10,6 +10,7 @@ android { namespace = "net.mullvad.mullvadvpn.feature.appinfo.impl" } dependencies { implementation(projects.lib.feature.appinfo.api) + implementation(projects.lib.feature.applisting.api) implementation(projects.lib.repository) implementation(libs.koin.compose) diff --git a/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/AppInfoViewModel.kt b/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/AppInfoViewModel.kt index 7c39a1d9f8..86c01717d0 100644 --- a/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/AppInfoViewModel.kt +++ b/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/AppInfoViewModel.kt @@ -1,6 +1,5 @@ package net.mullvad.mullvadvpn.feature.appinfo.impl -import android.content.res.Resources import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -12,18 +11,15 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import net.mullvad.mullvadvpn.feature.applisting.api.ResolveAppListingUseCase import net.mullvad.mullvadvpn.lib.common.Lc import net.mullvad.mullvadvpn.lib.common.constant.VIEW_MODEL_STOP_TIMEOUT -import net.mullvad.mullvadvpn.lib.model.PackageName import net.mullvad.mullvadvpn.lib.repository.AppVersionInfoRepository -import net.mullvad.mullvadvpn.lib.ui.resource.R class AppInfoViewModel( appVersionInfoRepository: AppVersionInfoRepository, - private val resources: Resources, private val isPlayBuild: Boolean, - private val isFdroidBuild: Boolean, - private val self: PackageName, + private val resolveAppListing: ResolveAppListingUseCase, ) : ViewModel() { private val _uiSideEffect = Channel<AppInfoSideEffect>() @@ -39,18 +35,12 @@ class AppInfoViewModel( ) fun openAppListing() = viewModelScope.launch { + val target = resolveAppListing() val sideEffect = - if (isPlayBuild || isFdroidBuild) { - AppInfoSideEffect.OpenUri( - uri = resources.getString(R.string.market_uri, self.value).toUri(), - errorMessage = resources.getString(R.string.uri_market_app_not_found), - ) - } else { - AppInfoSideEffect.OpenUri( - uri = resources.getString(R.string.download_url).toUri(), - errorMessage = resources.getString(R.string.uri_browser_app_not_found), - ) - } + AppInfoSideEffect.OpenUri( + uri = target.listingUri.toUri(), + errorMessage = target.errorMessage, + ) _uiSideEffect.send(sideEffect) } } diff --git a/android/lib/feature/applisting/api/build.gradle.kts b/android/lib/feature/applisting/api/build.gradle.kts new file mode 100644 index 0000000000..c98931c67f --- /dev/null +++ b/android/lib/feature/applisting/api/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { alias(libs.plugins.mullvad.android.library.feature.api) } + +android { namespace = "net.mullvad.mullvadvpn.feature.applisting.api" } diff --git a/android/lib/feature/applisting/api/src/main/kotlin/net/mullvad/mullvadvpn/feature/applisting/api/AppListingTarget.kt b/android/lib/feature/applisting/api/src/main/kotlin/net/mullvad/mullvadvpn/feature/applisting/api/AppListingTarget.kt new file mode 100644 index 0000000000..2275539ebf --- /dev/null +++ b/android/lib/feature/applisting/api/src/main/kotlin/net/mullvad/mullvadvpn/feature/applisting/api/AppListingTarget.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.feature.applisting.api + +data class AppListingTarget(val listingUri: String, val errorMessage: String) diff --git a/android/lib/feature/applisting/api/src/main/kotlin/net/mullvad/mullvadvpn/feature/applisting/api/ResolveAppListingUseCase.kt b/android/lib/feature/applisting/api/src/main/kotlin/net/mullvad/mullvadvpn/feature/applisting/api/ResolveAppListingUseCase.kt new file mode 100644 index 0000000000..0766b4f5c7 --- /dev/null +++ b/android/lib/feature/applisting/api/src/main/kotlin/net/mullvad/mullvadvpn/feature/applisting/api/ResolveAppListingUseCase.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.feature.applisting.api + +fun interface ResolveAppListingUseCase { + operator fun invoke(): AppListingTarget +} diff --git a/android/lib/feature/applisting/impl/build.gradle.kts b/android/lib/feature/applisting/impl/build.gradle.kts new file mode 100644 index 0000000000..152046e92b --- /dev/null +++ b/android/lib/feature/applisting/impl/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + alias(libs.plugins.mullvad.android.library) + alias(libs.plugins.mullvad.android.library.feature.impl) +} + +android { namespace = "net.mullvad.mullvadvpn.feature.applisting.impl" } + +dependencies { implementation(projects.lib.feature.applisting.api) } diff --git a/android/lib/feature/applisting/impl/src/main/AndroidManifest.xml b/android/lib/feature/applisting/impl/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..8bdb7e14b3 --- /dev/null +++ b/android/lib/feature/applisting/impl/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> + +</manifest> diff --git a/android/lib/feature/applisting/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/applisting/impl/AndroidInstallSourceProvider.kt b/android/lib/feature/applisting/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/applisting/impl/AndroidInstallSourceProvider.kt new file mode 100644 index 0000000000..0e48140df9 --- /dev/null +++ b/android/lib/feature/applisting/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/applisting/impl/AndroidInstallSourceProvider.kt @@ -0,0 +1,31 @@ +package net.mullvad.mullvadvpn.feature.applisting.impl + +import android.content.Context +import android.content.pm.PackageInstaller +import android.content.pm.PackageManager +import android.os.Build + +class AndroidInstallSourceProvider(private val context: Context) : InstallSourceProvider { + override fun isInstalledFromStore(): Boolean { + val packageName = context.packageName + val packageManager = context.packageManager + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> + try { + packageManager.getInstallSourceInfo(packageName).packageSource == + PackageInstaller.PACKAGE_SOURCE_STORE + } catch (_: PackageManager.NameNotFoundException) { + false + } + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> + try { + packageManager.getInstallSourceInfo(packageName).installingPackageName != null + } catch (_: PackageManager.NameNotFoundException) { + false + } + else -> + @Suppress("DEPRECATION") + (packageManager.getInstallerPackageName(packageName) != null) + } + } +} diff --git a/android/lib/feature/applisting/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/applisting/impl/InstallSourceProvider.kt b/android/lib/feature/applisting/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/applisting/impl/InstallSourceProvider.kt new file mode 100644 index 0000000000..c1b8910c74 --- /dev/null +++ b/android/lib/feature/applisting/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/applisting/impl/InstallSourceProvider.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.feature.applisting.impl + +fun interface InstallSourceProvider { + fun isInstalledFromStore(): Boolean +} diff --git a/android/lib/feature/applisting/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/applisting/impl/ResolveAppListingUseCaseImpl.kt b/android/lib/feature/applisting/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/applisting/impl/ResolveAppListingUseCaseImpl.kt new file mode 100644 index 0000000000..0fa6058407 --- /dev/null +++ b/android/lib/feature/applisting/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/applisting/impl/ResolveAppListingUseCaseImpl.kt @@ -0,0 +1,27 @@ +package net.mullvad.mullvadvpn.feature.applisting.impl + +import android.content.res.Resources +import net.mullvad.mullvadvpn.feature.applisting.api.AppListingTarget +import net.mullvad.mullvadvpn.feature.applisting.api.ResolveAppListingUseCase +import net.mullvad.mullvadvpn.lib.model.PackageName +import net.mullvad.mullvadvpn.lib.ui.resource.R + +class ResolveAppListingUseCaseImpl( + private val resources: Resources, + private val packageName: PackageName, + private val isPlayBuild: Boolean, + private val installSourceProvider: InstallSourceProvider, +) : ResolveAppListingUseCase { + override fun invoke(): AppListingTarget = + if (isPlayBuild || installSourceProvider.isInstalledFromStore()) { + AppListingTarget( + listingUri = resources.getString(R.string.market_uri, packageName.value), + errorMessage = resources.getString(R.string.uri_market_app_not_found), + ) + } else { + AppListingTarget( + listingUri = resources.getString(R.string.download_url), + errorMessage = resources.getString(R.string.uri_browser_app_not_found), + ) + } +} diff --git a/android/lib/feature/applisting/impl/src/test/kotlin/net/mullvad/mullvadvpn/feature/applisting/impl/ResolveAppListingUseCaseImplTest.kt b/android/lib/feature/applisting/impl/src/test/kotlin/net/mullvad/mullvadvpn/feature/applisting/impl/ResolveAppListingUseCaseImplTest.kt new file mode 100644 index 0000000000..495b30db51 --- /dev/null +++ b/android/lib/feature/applisting/impl/src/test/kotlin/net/mullvad/mullvadvpn/feature/applisting/impl/ResolveAppListingUseCaseImplTest.kt @@ -0,0 +1,79 @@ +package net.mullvad.mullvadvpn.feature.applisting.impl + +import android.content.res.Resources +import io.mockk.every +import io.mockk.mockk +import kotlin.test.assertEquals +import net.mullvad.mullvadvpn.lib.model.PackageName +import net.mullvad.mullvadvpn.lib.ui.resource.R +import org.junit.jupiter.api.Test + +class ResolveAppListingUseCaseImplTest { + + private val mockResources: Resources = mockk() + + @Test + fun `when play build should return market uri`() { + // Arrange + val useCase = createUseCase(isPlayBuild = true, isStoreInstall = false) + + // Act + val result = useCase() + + // Assert + assertEquals(MARKET_URI, result.listingUri) + assertEquals(MARKET_ERROR, result.errorMessage) + } + + @Test + fun `when store install should return market uri`() { + // Arrange + val useCase = createUseCase(isPlayBuild = false, isStoreInstall = true) + + // Act + val result = useCase() + + // Assert + assertEquals(MARKET_URI, result.listingUri) + assertEquals(MARKET_ERROR, result.errorMessage) + } + + @Test + fun `when sideloaded build should return download url`() { + // Arrange + val useCase = createUseCase(isPlayBuild = false, isStoreInstall = false) + + // Act + val result = useCase() + + // Assert + assertEquals(DOWNLOAD_URL, result.listingUri) + assertEquals(BROWSER_ERROR, result.errorMessage) + } + + private fun createUseCase( + isPlayBuild: Boolean, + isStoreInstall: Boolean, + ): ResolveAppListingUseCaseImpl { + every { mockResources.getString(R.string.market_uri, PACKAGE_NAME.value) } returns + MARKET_URI + every { mockResources.getString(R.string.download_url) } returns DOWNLOAD_URL + every { mockResources.getString(R.string.uri_market_app_not_found) } returns MARKET_ERROR + every { mockResources.getString(R.string.uri_browser_app_not_found) } returns BROWSER_ERROR + + return ResolveAppListingUseCaseImpl( + resources = mockResources, + packageName = PACKAGE_NAME, + isPlayBuild = isPlayBuild, + installSourceProvider = InstallSourceProvider { isStoreInstall }, + ) + } + + companion object { + private val PACKAGE_NAME = PackageName("net.mullvad.mullvadvpn") + private const val MARKET_URI = "market://details?id=net.mullvad.mullvadvpn" + private const val DOWNLOAD_URL = "https://mullvad.net/download/vpn/android" + private const val MARKET_ERROR = "No Android app store installed, could not open link" + private const val BROWSER_ERROR = "No browser app installed, could not open link" + } +} diff --git a/android/lib/feature/home/impl/build.gradle.kts b/android/lib/feature/home/impl/build.gradle.kts index ee7b90f085..7b27e30b81 100644 --- a/android/lib/feature/home/impl/build.gradle.kts +++ b/android/lib/feature/home/impl/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(projects.lib.feature.addtime.impl) implementation(projects.lib.feature.anticensorship.api) implementation(projects.lib.feature.appinfo.api) + implementation(projects.lib.feature.applisting.api) implementation(projects.lib.feature.daita.api) implementation(projects.lib.feature.home.api) implementation(projects.lib.feature.location.api) diff --git a/android/lib/feature/home/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/home/impl/connect/ConnectViewModel.kt b/android/lib/feature/home/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/home/impl/connect/ConnectViewModel.kt index eae192ccad..18efcb3637 100644 --- a/android/lib/feature/home/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/home/impl/connect/ConnectViewModel.kt +++ b/android/lib/feature/home/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/home/impl/connect/ConnectViewModel.kt @@ -1,6 +1,5 @@ package net.mullvad.mullvadvpn.feature.home.impl.connect -import android.content.res.Resources import android.net.Uri import androidx.core.net.toUri import androidx.lifecycle.ViewModel @@ -18,6 +17,7 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import net.mullvad.mullvadvpn.feature.applisting.api.ResolveAppListingUseCase import net.mullvad.mullvadvpn.feature.home.impl.connect.notificationbanner.InAppNotificationController import net.mullvad.mullvadvpn.lib.common.constant.VIEW_MODEL_STOP_TIMEOUT import net.mullvad.mullvadvpn.lib.common.util.combine @@ -27,7 +27,6 @@ import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect import net.mullvad.mullvadvpn.lib.model.ConnectError import net.mullvad.mullvadvpn.lib.model.DeviceState import net.mullvad.mullvadvpn.lib.model.DisconnectReason -import net.mullvad.mullvadvpn.lib.model.PackageName import net.mullvad.mullvadvpn.lib.model.PrepareError import net.mullvad.mullvadvpn.lib.model.TunnelState import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken @@ -39,7 +38,6 @@ import net.mullvad.mullvadvpn.lib.repository.DeviceRepository import net.mullvad.mullvadvpn.lib.repository.NewDeviceRepository import net.mullvad.mullvadvpn.lib.repository.PaymentLogic import net.mullvad.mullvadvpn.lib.repository.UserPreferencesRepository -import net.mullvad.mullvadvpn.lib.ui.resource.R import net.mullvad.mullvadvpn.lib.usecase.LastKnownLocationUseCase import net.mullvad.mullvadvpn.lib.usecase.OutOfTimeUseCase import net.mullvad.mullvadvpn.lib.usecase.SelectedLocationTitleUseCase @@ -59,10 +57,8 @@ class ConnectViewModel( private val connectionProxy: ConnectionProxy, lastKnownLocationUseCase: LastKnownLocationUseCase, private val systemVpnSettingsUseCase: SystemVpnSettingsAvailableUseCase, - private val resources: Resources, private val isPlayBuild: Boolean, - private val isFdroidBuild: Boolean, - private val self: PackageName, + private val resolveAppListing: ResolveAppListingUseCase, ) : ViewModel() { private val _uiSideEffect = Channel<UiSideEffect>() @@ -198,18 +194,12 @@ class ConnectViewModel( } fun openAppListing() = viewModelScope.launch { + val target = resolveAppListing() val sideEffect = - if (isPlayBuild || isFdroidBuild) { - UiSideEffect.OpenUri( - uri = resources.getString(R.string.market_uri, self.value).toUri(), - errorMessage = resources.getString(R.string.uri_market_app_not_found), - ) - } else { - UiSideEffect.OpenUri( - uri = resources.getString(R.string.download_url).toUri(), - errorMessage = resources.getString(R.string.uri_browser_app_not_found), - ) - } + UiSideEffect.OpenUri( + uri = target.listingUri.toUri(), + errorMessage = target.errorMessage, + ) _uiSideEffect.send(sideEffect) } diff --git a/android/lib/feature/home/impl/src/test/kotlin/net/mullvad/mullvadvpn/feature/home/impl/connect/ConnectViewModelTest.kt b/android/lib/feature/home/impl/src/test/kotlin/net/mullvad/mullvadvpn/feature/home/impl/connect/ConnectViewModelTest.kt index 141d2e6695..74291039de 100644 --- a/android/lib/feature/home/impl/src/test/kotlin/net/mullvad/mullvadvpn/feature/home/impl/connect/ConnectViewModelTest.kt +++ b/android/lib/feature/home/impl/src/test/kotlin/net/mullvad/mullvadvpn/feature/home/impl/connect/ConnectViewModelTest.kt @@ -26,7 +26,6 @@ import net.mullvad.mullvadvpn.lib.model.DisconnectReason import net.mullvad.mullvadvpn.lib.model.ErrorState import net.mullvad.mullvadvpn.lib.model.GeoIpLocation import net.mullvad.mullvadvpn.lib.model.InAppNotification -import net.mullvad.mullvadvpn.lib.model.PackageName import net.mullvad.mullvadvpn.lib.model.TunnelEndpoint import net.mullvad.mullvadvpn.lib.model.TunnelState import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken @@ -124,10 +123,8 @@ class ConnectViewModelTest { connectionProxy = mockConnectionProxy, lastKnownLocationUseCase = mockLastKnownLocationUseCase, systemVpnSettingsUseCase = mockSystemVpnSettingsUseCase, - resources = mockk(), isPlayBuild = false, - isFdroidBuild = false, - self = PackageName("net.mullvad.mullvadvpn"), + resolveAppListing = mockk(), ) } diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index 06c92449e9..95b89e9b52 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -46,6 +46,8 @@ include( ":lib:feature:appicon:api", ":lib:feature:appinfo:impl", ":lib:feature:appinfo:api", + ":lib:feature:applisting:impl", + ":lib:feature:applisting:api", ":lib:feature:appearance:impl", ":lib:feature:appearance:api", ":lib:feature:autoconnect:impl", |
