Loading app/src/main/java/foundation/e/apps/data/application/downloadInfo/DownloadInfoApiImpl.kt +38 −8 Original line number Diff line number Diff line Loading @@ -23,10 +23,12 @@ import foundation.e.apps.data.cleanapk.CleanApkDownloadInfoFetcher import foundation.e.apps.data.enums.Source import foundation.e.apps.data.handleNetworkResult import foundation.e.apps.data.installation.model.AppInstall import foundation.e.apps.data.playstore.InstalledAppCertificateHashProvider import javax.inject.Inject class DownloadInfoApiImpl @Inject constructor( private val appSources: AppSourcesContainer private val appSources: AppSourcesContainer, private val installedAppCertificateHashProvider: InstalledAppCertificateHashProvider, ) : DownloadInfoApi { override suspend fun getOnDemandModule( Loading Loading @@ -85,19 +87,19 @@ class DownloadInfoApiImpl @Inject constructor( appInstall: AppInstall, list: MutableList<String> ) { val downloadList = appSources.gplayRepo.getDownloadInfo( appInstall.packageName, appInstall.versionCode, appInstall.offerType ) val certificateHash = getUpdateCertificateHash(appInstall) val downloadList = getMainAppDownloadInfo(appInstall, certificateHash) appInstall.files = downloadList list.addAll(downloadList.map { it.url }) appInstall.sharedLibs.forEach { lib -> if (lib.downloadUrls.isEmpty()) { val libFiles = runCatching { appSources.gplayRepo.getDownloadInfo(lib.packageName, lib.versionCode, lib.offerType) appSources.gplayRepo.getDownloadInfo( lib.packageName, lib.versionCode, lib.offerType, ) }.getOrElse { error( "Cannot install ${appInstall.packageName}: " + Loading @@ -115,6 +117,34 @@ class DownloadInfoApiImpl @Inject constructor( } } private suspend fun getMainAppDownloadInfo( appInstall: AppInstall, certificateHash: String?, ) = if (certificateHash.isNullOrBlank()) { appSources.gplayRepo.getDownloadInfo( appInstall.packageName, appInstall.versionCode, appInstall.offerType, ) } else { appSources.gplayRepo.getDownloadInfo( appInstall.packageName, appInstall.versionCode, appInstall.offerType, certificateHash, ) } private fun getUpdateCertificateHash(appInstall: AppInstall): String? { if (!appInstall.isUpdateRequest) { return null } return installedAppCertificateHashProvider.getLatestEncodedCertificateHash( appInstall.packageName, ) } private suspend fun updateDownloadInfoFromCleanApk( appInstall: AppInstall, list: MutableList<String> Loading app/src/main/java/foundation/e/apps/data/install/pkg/AppLoungePackageManager.kt +93 −2 Original line number Diff line number Diff line Loading @@ -39,6 +39,9 @@ import foundation.e.apps.OpenForTesting import foundation.e.apps.data.installation.model.AppInstall import foundation.e.apps.data.installation.model.InstallationSource import foundation.e.apps.data.installation.model.InstallationType import foundation.e.apps.data.signing.SigningCertificateDigests import foundation.e.apps.data.signing.toPlayStoreDeliveryCertificateHash import foundation.e.apps.data.signing.toSha256SigningDigest import foundation.e.apps.domain.model.install.Status import timber.log.Timber import java.io.File Loading Loading @@ -141,8 +144,17 @@ class AppLoungePackageManager @Inject constructor( fun getInstallerName(packageName: String): String { return try { when { packageName == context.packageName -> context.packageName Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> { val installerInfo = packageManager.getInstallSourceInfo(packageName) installerInfo.originatingPackageName ?: installerInfo.installingPackageName ?: UNKNOWN_VALUE installerInfo.updateOwnerPackageName ?: installerInfo.installingPackageName ?: UNKNOWN_VALUE } else -> packageManager.getInstallSourceInfo(packageName).installingPackageName ?: UNKNOWN_VALUE } } catch (e: NameNotFoundException) { Timber.e("getInstallerName -> $packageName : ${e.localizedMessage}") UNKNOWN_VALUE Loading Loading @@ -170,6 +182,85 @@ class AppLoungePackageManager @Inject constructor( return packageInfo?.versionName ?: UNKNOWN_VALUE } fun getPlayStoreDeliveryCertificateHashes(packageName: String): Set<String> { return getSigningCertificateDigests(packageName).playStoreDeliveryHashes } fun getCurrentPlayStoreDeliveryCertificateHash(packageName: String): String? { val packageInfo = getPackageInfoWithSigningCertificates(packageName) ?: return null return getCurrentSigningCertificateBytes(packageInfo) .minByOrNull { it.toSha256SigningDigest() } ?.toPlayStoreDeliveryCertificateHash() } fun getSigningCertificateDigests(packageName: String): SigningCertificateDigests { val packageInfo = getPackageInfoWithSigningCertificates(packageName) ?: return SigningCertificateDigests() val signingCertificateBytes = getSigningCertificateBytes(packageInfo) return SigningCertificateDigests( sha256Digests = signingCertificateBytes .mapTo(linkedSetOf()) { it.toSha256SigningDigest() }, playStoreDeliveryHashes = signingCertificateBytes .mapTo(linkedSetOf()) { it.toPlayStoreDeliveryCertificateHash() }, ) } private fun getCurrentSigningCertificateBytes(packageInfo: PackageInfo): List<ByteArray> { val signingInfo = packageInfo.signingInfo ?: return emptyList() val currentSigners = if (signingInfo.hasMultipleSigners()) { signingInfo.apkContentsSigners.orEmpty().toList() } else { signingInfo.signingCertificateHistory.orEmpty().takeLast(1) } return currentSigners.map { it.toByteArray() } } private fun getSigningCertificateBytes(packageInfo: PackageInfo): List<ByteArray> { val signingInfo = packageInfo.signingInfo ?: return emptyList() return if (signingInfo.hasMultipleSigners()) { signingInfo.apkContentsSigners } else { signingInfo.signingCertificateHistory }.orEmpty() .map { it.toByteArray() } } private fun getPackageInfoWithSigningCertificates(packageName: String): PackageInfo? { return try { when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> { packageManager.getPackageInfo( packageName, PackageManager.PackageInfoFlags.of( PackageManager.GET_SIGNING_CERTIFICATES.toLong(), ), ) } else -> { @Suppress("DEPRECATION") packageManager.getPackageInfo( packageName, PackageManager.GET_SIGNING_CERTIFICATES, ) } } } catch (e: NameNotFoundException) { Timber.e( "getSigningCertificateDigests -> $packageName : ${e.localizedMessage}" ) null } catch (e: IllegalArgumentException) { Timber.e( "getSigningCertificateDigests -> $packageName : ${e.localizedMessage}" ) null } } /** * Installs the given package using system API. * When [sharedLibs] is non-empty, shared libraries are staged and committed sequentially Loading app/src/main/java/foundation/e/apps/data/playstore/InstalledAppCertificateHashProvider.kt 0 → 100644 +38 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 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.apps.data.playstore import foundation.e.apps.data.install.pkg.AppLoungePackageManager import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @Singleton class InstalledAppCertificateHashProvider @Inject constructor( private val appLoungePackageManager: AppLoungePackageManager, ) { fun getLatestEncodedCertificateHash(packageName: String): String? { return runCatching { appLoungePackageManager .getCurrentPlayStoreDeliveryCertificateHash(packageName) }.onFailure { Timber.w(it, "Failed to get certificate hash for %s", packageName) }.getOrNull() } } app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt +22 −3 Original line number Diff line number Diff line Loading @@ -458,7 +458,8 @@ class PlayStoreRepository @Inject constructor( suspend fun getDownloadInfo( idOrPackageName: String, versionCode: Long, offerType: Int offerType: Int, certificateHash: String? = null, ): List<PlayFile> = withContext(Dispatchers.IO) { var version = versionCode var offer = offerType Loading @@ -481,7 +482,20 @@ class PlayStoreRepository @Inject constructor( val purchaseHelper = PurchaseHelper(playStoreAuthManager.requireValidatedPlayStoreAuth()) .using(gPlayHttpClient) buildList { addAll(purchaseHelper.purchase(idOrPackageName, version, offer)) } buildList { if (certificateHash.isNullOrBlank()) { addAll(purchaseHelper.purchase(idOrPackageName, version, offer)) } else { addAll( purchaseHelper.purchase( idOrPackageName, version, offer, certificateHash, ) ) } } }, ) } Loading @@ -498,7 +512,12 @@ class PlayStoreRepository @Inject constructor( request = { val purchaseHelper = PurchaseHelper(playStoreAuthManager.requireValidatedPlayStoreAuth()) .using(gPlayHttpClient) purchaseHelper.purchase(packageName, versionCode, offerType, moduleName) purchaseHelper.purchase( packageName = packageName, versionCode = versionCode, offerType = offerType, splitModule = moduleName, ) }, ) } Loading app/src/test/java/foundation/e/apps/data/application/downloadInfo/DownloadInfoApiImplTest.kt +51 −1 Original line number Diff line number Diff line Loading @@ -25,11 +25,13 @@ import foundation.e.apps.data.cleanapk.repositories.CleanApkPwaRepository import foundation.e.apps.data.enums.Source import foundation.e.apps.data.installation.model.AppInstall import foundation.e.apps.data.installation.model.SharedLib import foundation.e.apps.data.playstore.InstalledAppCertificateHashProvider import foundation.e.apps.data.playstore.PlayStoreRepository import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import kotlin.test.assertFailsWith Loading @@ -42,13 +44,15 @@ class DownloadInfoApiImplTest { private val gplayRepo = mockk<PlayStoreRepository>() private val cleanApkAppsRepo = mockk<CleanApkAppsRepository>() private val cleanApkPwaRepo = mockk<CleanApkPwaRepository>() private val installedAppCertificateHashProvider = mockk<InstalledAppCertificateHashProvider>() private lateinit var downloadInfoApi: DownloadInfoApiImpl @Before fun setUp() { downloadInfoApi = DownloadInfoApiImpl( AppSourcesContainer(gplayRepo, cleanApkAppsRepo, cleanApkPwaRepo) AppSourcesContainer(gplayRepo, cleanApkAppsRepo, cleanApkPwaRepo), installedAppCertificateHashProvider, ) } Loading Loading @@ -165,6 +169,52 @@ class DownloadInfoApiImplTest { assertThat(error.cause).isNull() } @Test fun updateFusedDownloadWithDownloadingInfo_usesInstalledCertificateHashForPlayUpdate() = runTest { val appInstall = AppInstall( packageName = "com.example.app", versionCode = 99L, offerType = 1, isUpdateRequest = true, ) val mainFiles = listOf(playFile("https://example.org/app-base.apk")) every { installedAppCertificateHashProvider.getLatestEncodedCertificateHash("com.example.app") } returns "encoded-cert-hash" coEvery { gplayRepo.getDownloadInfo("com.example.app", 99L, 1, "encoded-cert-hash") } returns mainFiles downloadInfoApi.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall) assertThat(appInstall.downloadURLList).containsExactly("https://example.org/app-base.apk") coVerify(exactly = 1) { gplayRepo.getDownloadInfo("com.example.app", 99L, 1, "encoded-cert-hash") } } @Test fun updateFusedDownloadWithDownloadingInfo_skipsInstalledCertificateHashForPlayInstall() = runTest { val appInstall = AppInstall( packageName = "com.example.app", versionCode = 99L, offerType = 1, isUpdateRequest = false, ) val mainFiles = listOf(playFile("https://example.org/app-base.apk")) coEvery { gplayRepo.getDownloadInfo("com.example.app", 99L, 1) } returns mainFiles downloadInfoApi.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall) assertThat(appInstall.downloadURLList).containsExactly("https://example.org/app-base.apk") verify(exactly = 0) { installedAppCertificateHashProvider.getLatestEncodedCertificateHash(any()) } coVerify(exactly = 1) { gplayRepo.getDownloadInfo("com.example.app", 99L, 1) } } private fun playFile(url: String): PlayFile { val file = mockk<PlayFile>() every { file.url } returns url Loading Loading
app/src/main/java/foundation/e/apps/data/application/downloadInfo/DownloadInfoApiImpl.kt +38 −8 Original line number Diff line number Diff line Loading @@ -23,10 +23,12 @@ import foundation.e.apps.data.cleanapk.CleanApkDownloadInfoFetcher import foundation.e.apps.data.enums.Source import foundation.e.apps.data.handleNetworkResult import foundation.e.apps.data.installation.model.AppInstall import foundation.e.apps.data.playstore.InstalledAppCertificateHashProvider import javax.inject.Inject class DownloadInfoApiImpl @Inject constructor( private val appSources: AppSourcesContainer private val appSources: AppSourcesContainer, private val installedAppCertificateHashProvider: InstalledAppCertificateHashProvider, ) : DownloadInfoApi { override suspend fun getOnDemandModule( Loading Loading @@ -85,19 +87,19 @@ class DownloadInfoApiImpl @Inject constructor( appInstall: AppInstall, list: MutableList<String> ) { val downloadList = appSources.gplayRepo.getDownloadInfo( appInstall.packageName, appInstall.versionCode, appInstall.offerType ) val certificateHash = getUpdateCertificateHash(appInstall) val downloadList = getMainAppDownloadInfo(appInstall, certificateHash) appInstall.files = downloadList list.addAll(downloadList.map { it.url }) appInstall.sharedLibs.forEach { lib -> if (lib.downloadUrls.isEmpty()) { val libFiles = runCatching { appSources.gplayRepo.getDownloadInfo(lib.packageName, lib.versionCode, lib.offerType) appSources.gplayRepo.getDownloadInfo( lib.packageName, lib.versionCode, lib.offerType, ) }.getOrElse { error( "Cannot install ${appInstall.packageName}: " + Loading @@ -115,6 +117,34 @@ class DownloadInfoApiImpl @Inject constructor( } } private suspend fun getMainAppDownloadInfo( appInstall: AppInstall, certificateHash: String?, ) = if (certificateHash.isNullOrBlank()) { appSources.gplayRepo.getDownloadInfo( appInstall.packageName, appInstall.versionCode, appInstall.offerType, ) } else { appSources.gplayRepo.getDownloadInfo( appInstall.packageName, appInstall.versionCode, appInstall.offerType, certificateHash, ) } private fun getUpdateCertificateHash(appInstall: AppInstall): String? { if (!appInstall.isUpdateRequest) { return null } return installedAppCertificateHashProvider.getLatestEncodedCertificateHash( appInstall.packageName, ) } private suspend fun updateDownloadInfoFromCleanApk( appInstall: AppInstall, list: MutableList<String> Loading
app/src/main/java/foundation/e/apps/data/install/pkg/AppLoungePackageManager.kt +93 −2 Original line number Diff line number Diff line Loading @@ -39,6 +39,9 @@ import foundation.e.apps.OpenForTesting import foundation.e.apps.data.installation.model.AppInstall import foundation.e.apps.data.installation.model.InstallationSource import foundation.e.apps.data.installation.model.InstallationType import foundation.e.apps.data.signing.SigningCertificateDigests import foundation.e.apps.data.signing.toPlayStoreDeliveryCertificateHash import foundation.e.apps.data.signing.toSha256SigningDigest import foundation.e.apps.domain.model.install.Status import timber.log.Timber import java.io.File Loading Loading @@ -141,8 +144,17 @@ class AppLoungePackageManager @Inject constructor( fun getInstallerName(packageName: String): String { return try { when { packageName == context.packageName -> context.packageName Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> { val installerInfo = packageManager.getInstallSourceInfo(packageName) installerInfo.originatingPackageName ?: installerInfo.installingPackageName ?: UNKNOWN_VALUE installerInfo.updateOwnerPackageName ?: installerInfo.installingPackageName ?: UNKNOWN_VALUE } else -> packageManager.getInstallSourceInfo(packageName).installingPackageName ?: UNKNOWN_VALUE } } catch (e: NameNotFoundException) { Timber.e("getInstallerName -> $packageName : ${e.localizedMessage}") UNKNOWN_VALUE Loading Loading @@ -170,6 +182,85 @@ class AppLoungePackageManager @Inject constructor( return packageInfo?.versionName ?: UNKNOWN_VALUE } fun getPlayStoreDeliveryCertificateHashes(packageName: String): Set<String> { return getSigningCertificateDigests(packageName).playStoreDeliveryHashes } fun getCurrentPlayStoreDeliveryCertificateHash(packageName: String): String? { val packageInfo = getPackageInfoWithSigningCertificates(packageName) ?: return null return getCurrentSigningCertificateBytes(packageInfo) .minByOrNull { it.toSha256SigningDigest() } ?.toPlayStoreDeliveryCertificateHash() } fun getSigningCertificateDigests(packageName: String): SigningCertificateDigests { val packageInfo = getPackageInfoWithSigningCertificates(packageName) ?: return SigningCertificateDigests() val signingCertificateBytes = getSigningCertificateBytes(packageInfo) return SigningCertificateDigests( sha256Digests = signingCertificateBytes .mapTo(linkedSetOf()) { it.toSha256SigningDigest() }, playStoreDeliveryHashes = signingCertificateBytes .mapTo(linkedSetOf()) { it.toPlayStoreDeliveryCertificateHash() }, ) } private fun getCurrentSigningCertificateBytes(packageInfo: PackageInfo): List<ByteArray> { val signingInfo = packageInfo.signingInfo ?: return emptyList() val currentSigners = if (signingInfo.hasMultipleSigners()) { signingInfo.apkContentsSigners.orEmpty().toList() } else { signingInfo.signingCertificateHistory.orEmpty().takeLast(1) } return currentSigners.map { it.toByteArray() } } private fun getSigningCertificateBytes(packageInfo: PackageInfo): List<ByteArray> { val signingInfo = packageInfo.signingInfo ?: return emptyList() return if (signingInfo.hasMultipleSigners()) { signingInfo.apkContentsSigners } else { signingInfo.signingCertificateHistory }.orEmpty() .map { it.toByteArray() } } private fun getPackageInfoWithSigningCertificates(packageName: String): PackageInfo? { return try { when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> { packageManager.getPackageInfo( packageName, PackageManager.PackageInfoFlags.of( PackageManager.GET_SIGNING_CERTIFICATES.toLong(), ), ) } else -> { @Suppress("DEPRECATION") packageManager.getPackageInfo( packageName, PackageManager.GET_SIGNING_CERTIFICATES, ) } } } catch (e: NameNotFoundException) { Timber.e( "getSigningCertificateDigests -> $packageName : ${e.localizedMessage}" ) null } catch (e: IllegalArgumentException) { Timber.e( "getSigningCertificateDigests -> $packageName : ${e.localizedMessage}" ) null } } /** * Installs the given package using system API. * When [sharedLibs] is non-empty, shared libraries are staged and committed sequentially Loading
app/src/main/java/foundation/e/apps/data/playstore/InstalledAppCertificateHashProvider.kt 0 → 100644 +38 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 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.apps.data.playstore import foundation.e.apps.data.install.pkg.AppLoungePackageManager import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @Singleton class InstalledAppCertificateHashProvider @Inject constructor( private val appLoungePackageManager: AppLoungePackageManager, ) { fun getLatestEncodedCertificateHash(packageName: String): String? { return runCatching { appLoungePackageManager .getCurrentPlayStoreDeliveryCertificateHash(packageName) }.onFailure { Timber.w(it, "Failed to get certificate hash for %s", packageName) }.getOrNull() } }
app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt +22 −3 Original line number Diff line number Diff line Loading @@ -458,7 +458,8 @@ class PlayStoreRepository @Inject constructor( suspend fun getDownloadInfo( idOrPackageName: String, versionCode: Long, offerType: Int offerType: Int, certificateHash: String? = null, ): List<PlayFile> = withContext(Dispatchers.IO) { var version = versionCode var offer = offerType Loading @@ -481,7 +482,20 @@ class PlayStoreRepository @Inject constructor( val purchaseHelper = PurchaseHelper(playStoreAuthManager.requireValidatedPlayStoreAuth()) .using(gPlayHttpClient) buildList { addAll(purchaseHelper.purchase(idOrPackageName, version, offer)) } buildList { if (certificateHash.isNullOrBlank()) { addAll(purchaseHelper.purchase(idOrPackageName, version, offer)) } else { addAll( purchaseHelper.purchase( idOrPackageName, version, offer, certificateHash, ) ) } } }, ) } Loading @@ -498,7 +512,12 @@ class PlayStoreRepository @Inject constructor( request = { val purchaseHelper = PurchaseHelper(playStoreAuthManager.requireValidatedPlayStoreAuth()) .using(gPlayHttpClient) purchaseHelper.purchase(packageName, versionCode, offerType, moduleName) purchaseHelper.purchase( packageName = packageName, versionCode = versionCode, offerType = offerType, splitModule = moduleName, ) }, ) } Loading
app/src/test/java/foundation/e/apps/data/application/downloadInfo/DownloadInfoApiImplTest.kt +51 −1 Original line number Diff line number Diff line Loading @@ -25,11 +25,13 @@ import foundation.e.apps.data.cleanapk.repositories.CleanApkPwaRepository import foundation.e.apps.data.enums.Source import foundation.e.apps.data.installation.model.AppInstall import foundation.e.apps.data.installation.model.SharedLib import foundation.e.apps.data.playstore.InstalledAppCertificateHashProvider import foundation.e.apps.data.playstore.PlayStoreRepository import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import kotlin.test.assertFailsWith Loading @@ -42,13 +44,15 @@ class DownloadInfoApiImplTest { private val gplayRepo = mockk<PlayStoreRepository>() private val cleanApkAppsRepo = mockk<CleanApkAppsRepository>() private val cleanApkPwaRepo = mockk<CleanApkPwaRepository>() private val installedAppCertificateHashProvider = mockk<InstalledAppCertificateHashProvider>() private lateinit var downloadInfoApi: DownloadInfoApiImpl @Before fun setUp() { downloadInfoApi = DownloadInfoApiImpl( AppSourcesContainer(gplayRepo, cleanApkAppsRepo, cleanApkPwaRepo) AppSourcesContainer(gplayRepo, cleanApkAppsRepo, cleanApkPwaRepo), installedAppCertificateHashProvider, ) } Loading Loading @@ -165,6 +169,52 @@ class DownloadInfoApiImplTest { assertThat(error.cause).isNull() } @Test fun updateFusedDownloadWithDownloadingInfo_usesInstalledCertificateHashForPlayUpdate() = runTest { val appInstall = AppInstall( packageName = "com.example.app", versionCode = 99L, offerType = 1, isUpdateRequest = true, ) val mainFiles = listOf(playFile("https://example.org/app-base.apk")) every { installedAppCertificateHashProvider.getLatestEncodedCertificateHash("com.example.app") } returns "encoded-cert-hash" coEvery { gplayRepo.getDownloadInfo("com.example.app", 99L, 1, "encoded-cert-hash") } returns mainFiles downloadInfoApi.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall) assertThat(appInstall.downloadURLList).containsExactly("https://example.org/app-base.apk") coVerify(exactly = 1) { gplayRepo.getDownloadInfo("com.example.app", 99L, 1, "encoded-cert-hash") } } @Test fun updateFusedDownloadWithDownloadingInfo_skipsInstalledCertificateHashForPlayInstall() = runTest { val appInstall = AppInstall( packageName = "com.example.app", versionCode = 99L, offerType = 1, isUpdateRequest = false, ) val mainFiles = listOf(playFile("https://example.org/app-base.apk")) coEvery { gplayRepo.getDownloadInfo("com.example.app", 99L, 1) } returns mainFiles downloadInfoApi.updateFusedDownloadWithDownloadingInfo(Source.PLAY_STORE, appInstall) assertThat(appInstall.downloadURLList).containsExactly("https://example.org/app-base.apk") verify(exactly = 0) { installedAppCertificateHashProvider.getLatestEncodedCertificateHash(any()) } coVerify(exactly = 1) { gplayRepo.getDownloadInfo("com.example.app", 99L, 1) } } private fun playFile(url: String): PlayFile { val file = mockk<PlayFile>() every { file.url } returns url Loading