diff --git a/app/build.gradle b/app/build.gradle index 3c647b58d76e74d92c31f17f8d8d3d8a8e9b6a54..a4eed049fbae15bd6b1bf0bcf9c95149d39abfda 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -150,6 +150,9 @@ dependencies { //logger implementation 'com.jakewharton.timber:timber:5.0.1' + // Bouncy Castle + implementation 'org.bouncycastle:bcpg-jdk15on:1.60' + // Retrofit def retrofit_version = "2.9.0" implementation "com.squareup.retrofit2:retrofit:$retrofit_version" diff --git a/app/src/main/assets/f-droid.org-signing-key.gpg b/app/src/main/assets/f-droid.org-signing-key.gpg new file mode 100644 index 0000000000000000000000000000000000000000..16f1f017acb1872f6ff074a434f23edcb1427207 Binary files /dev/null and b/app/src/main/assets/f-droid.org-signing-key.gpg differ diff --git a/app/src/main/java/foundation/e/apps/api/cleanapk/ApkSignatureManager.kt b/app/src/main/java/foundation/e/apps/api/cleanapk/ApkSignatureManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..f9bd5443639eb6523efafa4ddfc1afeb754e08b7 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/api/cleanapk/ApkSignatureManager.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2022 ECORP + * + * 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 . + */ + +package foundation.e.apps.api.cleanapk + +import android.content.Context +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.openpgp.PGPCompressedData +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPSignatureList +import org.bouncycastle.openpgp.PGPUtil +import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory +import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator +import java.io.BufferedInputStream +import java.io.FileInputStream +import java.io.InputStream +import java.security.Security + +object ApkSignatureManager { + fun verifyFdroidSignature(context: Context, apkFilePath: String, signature: String): Boolean { + Security.addProvider(BouncyCastleProvider()) + return verifyAPKSignature( + BufferedInputStream(FileInputStream(apkFilePath)), + signature.byteInputStream(Charsets.UTF_8), + context.assets.open("f-droid.org-signing-key.gpg") + ) + } + + private fun verifyAPKSignature( + apkInputStream: BufferedInputStream, + apkSignatureInputStream: InputStream, + publicKeyInputStream: InputStream + ): Boolean { + try { + val signature = extractSignature(apkSignatureInputStream) + val pgpPublicKeyRingCollection = + PGPPublicKeyRingCollection( + PGPUtil.getDecoderStream(publicKeyInputStream), + JcaKeyFingerprintCalculator() + ) + + val key = pgpPublicKeyRingCollection.getPublicKey(signature.keyID) + signature.init(BcPGPContentVerifierBuilderProvider(), key) + updateSignature(apkInputStream, signature) + return signature.verify() + } catch (e: Exception) { + e.printStackTrace() + } finally { + apkInputStream.close() + apkSignatureInputStream.close() + publicKeyInputStream.close() + } + + return false + } + + private fun extractSignature(apkSignatureInputStream: InputStream): PGPSignature { + var jcaPGPObjectFactory = + JcaPGPObjectFactory(PGPUtil.getDecoderStream(apkSignatureInputStream)) + val pgpSignatureList: PGPSignatureList + + val pgpObject = jcaPGPObjectFactory.nextObject() + if (pgpObject is PGPCompressedData) { + jcaPGPObjectFactory = JcaPGPObjectFactory(pgpObject.dataStream) + pgpSignatureList = jcaPGPObjectFactory.nextObject() as PGPSignatureList + } else { + pgpSignatureList = pgpObject as PGPSignatureList + } + val signature = pgpSignatureList.get(0) + return signature + } + + private fun updateSignature( + apkInputStream: BufferedInputStream, + signature: PGPSignature + ) { + val buff = ByteArray(1024) + var read = apkInputStream.read(buff) + while (read != -1) { + signature.update(buff, 0, read) + read = apkInputStream.read(buff) + } + } +} diff --git a/app/src/main/java/foundation/e/apps/api/fdroid/FdroidApiInterface.kt b/app/src/main/java/foundation/e/apps/api/fdroid/FdroidApiInterface.kt index d002bc0842a8bc4952305d271a28c62dc9e423fb..b0e6fccf7b89fb3f6203083e57da8e9c1b26ba5e 100644 --- a/app/src/main/java/foundation/e/apps/api/fdroid/FdroidApiInterface.kt +++ b/app/src/main/java/foundation/e/apps/api/fdroid/FdroidApiInterface.kt @@ -1,6 +1,7 @@ package foundation.e.apps.api.fdroid import foundation.e.apps.api.fdroid.models.FdroidApiModel +import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path @@ -15,5 +16,5 @@ interface FdroidApiInterface { } @GET("{packageName}.yml") - suspend fun getFdroidInfoForPackage(@Path("packageName") packageName: String): FdroidApiModel? + suspend fun getFdroidInfoForPackage(@Path("packageName") packageName: String): Response } diff --git a/app/src/main/java/foundation/e/apps/api/fdroid/FdroidRepository.kt b/app/src/main/java/foundation/e/apps/api/fdroid/FdroidRepository.kt index d61d71791727450cfb272336828a9763f3439a98..7010aed4e2b44a7c7c1554ca14bb4becac44e955 100644 --- a/app/src/main/java/foundation/e/apps/api/fdroid/FdroidRepository.kt +++ b/app/src/main/java/foundation/e/apps/api/fdroid/FdroidRepository.kt @@ -1,5 +1,7 @@ package foundation.e.apps.api.fdroid +import android.content.Context +import foundation.e.apps.api.cleanapk.ApkSignatureManager import foundation.e.apps.api.fdroid.models.FdroidEntity import javax.inject.Inject import javax.inject.Singleton @@ -18,10 +20,21 @@ class FdroidRepository @Inject constructor( */ suspend fun getFdroidInfo(packageName: String): FdroidEntity? { return fdroidDao.getFdroidEntityFromPackageName(packageName) - ?: fdroidApi.getFdroidInfoForPackage(packageName)?.let { + ?: fdroidApi.getFdroidInfoForPackage(packageName).body()?.let { FdroidEntity(packageName, it.authorName).also { fdroidDao.saveFdroidEntity(it) } } } + + suspend fun isFdroidApplicationSigned(context: Context, packageName: String, apkFilePath: String, signature: String): Boolean { + if (isFdroidApplication(packageName)) { + return ApkSignatureManager.verifyFdroidSignature(context, apkFilePath, signature) + } + return false + } + + private suspend fun isFdroidApplication(packageName: String): Boolean { + return fdroidApi.getFdroidInfoForPackage(packageName).isSuccessful + } } diff --git a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt index 226c26de80d7952d2cd7a72775306a9421a13951..0d15c784bc9bf0fcf58543d998fa1f1e7cde0f40 100644 --- a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt +++ b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt @@ -78,6 +78,7 @@ class FusedAPIImpl @Inject constructor( companion object { private const val CATEGORY_TITLE_REPLACEABLE_CONJUNCTION = "&" + /* * Removing "private" access specifier to allow access in * MainActivityViewModel.timeoutAlertDialog @@ -184,7 +185,10 @@ class FusedAPIImpl @Inject constructor( * * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 */ - suspend fun getCategoriesList(type: Category.Type, authData: AuthData): Triple, String, ResultStatus> { + suspend fun getCategoriesList( + type: Category.Type, + authData: AuthData + ): Triple, String, ResultStatus> { val categoriesList = mutableListOf() val preferredApplicationType = preferenceManagerModule.preferredApplicationType() var apiStatus: ResultStatus = ResultStatus.OK @@ -242,7 +246,9 @@ class FusedAPIImpl @Inject constructor( gplayPackageResult = it.first } } - } catch (_: Exception) {} + } catch (e: Exception) { + Timber.e(e) + } } getCleanapkSearchResult(query).let { /* Cleanapk always returns something, it is never null. @@ -314,7 +320,12 @@ class FusedAPIImpl @Inject constructor( * If there had to be any timeout, it would already have happened * while fetching package specific results. */ - ResultSupreme.Success(Pair(filterWithKeywordSearch(it.first), it.second)) + ResultSupreme.Success( + Pair( + filterWithKeywordSearch(it.first), + it.second + ) + ) } ) } @@ -401,6 +412,7 @@ class FusedAPIImpl @Inject constructor( Origin.CLEANAPK -> { val downloadInfo = cleanAPKRepository.getDownloadInfo(fusedDownload.id).body() downloadInfo?.download_data?.download_link?.let { list.add(it) } + fusedDownload.signature = downloadInfo?.download_data?.signature ?: "" } Origin.GPLAY -> { val downloadList = gPlayAPIRepository.getDownloadInfo( @@ -453,7 +465,8 @@ class FusedAPIImpl @Inject constructor( ): ResultSupreme { var streamBundle = StreamBundle() val status = runCodeBlockWithTimeout({ - streamBundle = gPlayAPIRepository.getNextStreamBundle(authData, homeUrl, currentStreamBundle) + streamBundle = + gPlayAPIRepository.getNextStreamBundle(authData, homeUrl, currentStreamBundle) }) return ResultSupreme.create(status, streamBundle) } @@ -465,7 +478,8 @@ class FusedAPIImpl @Inject constructor( ): ResultSupreme { var streamCluster = StreamCluster() val status = runCodeBlockWithTimeout({ - streamCluster = gPlayAPIRepository.getAdjustedFirstCluster(authData, streamBundle, pointer) + streamCluster = + gPlayAPIRepository.getAdjustedFirstCluster(authData, streamBundle, pointer) }) return ResultSupreme.create(status, streamCluster) } @@ -481,7 +495,10 @@ class FusedAPIImpl @Inject constructor( return ResultSupreme.create(status, streamCluster) } - suspend fun getPlayStoreApps(browseUrl: String, authData: AuthData): ResultSupreme> { + suspend fun getPlayStoreApps( + browseUrl: String, + authData: AuthData + ): ResultSupreme> { val list = mutableListOf() val status = runCodeBlockWithTimeout({ list.addAll( @@ -669,6 +686,11 @@ class FusedAPIImpl @Inject constructor( return if (fusedApp.origin == Origin.GPLAY) FilterLevel.UNKNOWN else FilterLevel.NONE } + + if (!fusedApp.isFree && fusedApp.price.isBlank()) { + return FilterLevel.UI + } + if (fusedApp.restriction != Constants.Restriction.NOT_RESTRICTED) { /* * Check if app details can be shown. If not then remove the app from lists. @@ -693,13 +715,8 @@ class FusedAPIImpl @Inject constructor( } catch (e: Exception) { return FilterLevel.UI } - } else { - if (!fusedApp.isFree && fusedApp.price.isBlank()) { - return FilterLevel.UI - } - if (fusedApp.originalSize == 0L) { - return FilterLevel.UI - } + } else if (fusedApp.originalSize == 0L) { + return FilterLevel.UI } return FilterLevel.NONE } @@ -1275,7 +1292,7 @@ class FusedAPIImpl @Inject constructor( oldHomeData.forEach { val fusedHome = newHomeData[oldHomeData.indexOf(it)] - if (!it.title.contentEquals(fusedHome.title) || !areFusedAppsUpdated(it, fusedHome)) { + if (!it.title.contentEquals(fusedHome.title) || areFusedAppsUpdated(it, fusedHome)) { return true } } @@ -1287,15 +1304,18 @@ class FusedAPIImpl @Inject constructor( newFusedHome: FusedHome, ): Boolean { val fusedAppDiffUtil = HomeChildFusedAppDiffUtil() + if (oldFusedHome.list.size != newFusedHome.list.size) { + return true + } oldFusedHome.list.forEach { oldFusedApp -> val indexOfOldFusedApp = oldFusedHome.list.indexOf(oldFusedApp) val fusedApp = newFusedHome.list[indexOfOldFusedApp] if (!fusedAppDiffUtil.areContentsTheSame(oldFusedApp, fusedApp)) { - return false + return true } } - return true + return false } /** @@ -1324,7 +1344,8 @@ class FusedAPIImpl @Inject constructor( if (it.status == Status.INSTALLATION_ISSUE) { return@forEach } - val currentAppStatus = pkgManagerModule.getPackageStatus(it.package_name, it.latest_version_code) + val currentAppStatus = + pkgManagerModule.getPackageStatus(it.package_name, it.latest_version_code) if (it.status != currentAppStatus) { return true } diff --git a/app/src/main/java/foundation/e/apps/manager/database/FusedDatabase.kt b/app/src/main/java/foundation/e/apps/manager/database/FusedDatabase.kt index c8b4c080c32f4ad90fb9ccbd575150299398ccc2..3a307ef8859b45b1fd7a310b78ad14067a5fc3a0 100644 --- a/app/src/main/java/foundation/e/apps/manager/database/FusedDatabase.kt +++ b/app/src/main/java/foundation/e/apps/manager/database/FusedDatabase.kt @@ -9,7 +9,7 @@ import foundation.e.apps.api.database.AppDatabase import foundation.e.apps.manager.database.fusedDownload.FusedDownload import foundation.e.apps.manager.database.fusedDownload.FusedDownloadDAO -@Database(entities = [FusedDownload::class], version = 2, exportSchema = false) +@Database(entities = [FusedDownload::class], version = 3, exportSchema = false) @TypeConverters(FusedConverter::class) abstract class FusedDatabase : RoomDatabase() { abstract fun fusedDownloadDao(): FusedDownloadDAO diff --git a/app/src/main/java/foundation/e/apps/manager/database/fusedDownload/FusedDownload.kt b/app/src/main/java/foundation/e/apps/manager/database/fusedDownload/FusedDownload.kt index 269298dd463b89a71811b4a1a434d5f650a7e4e0..0cd910a68676c1b608d1ba0df8a38a710cdf9431 100644 --- a/app/src/main/java/foundation/e/apps/manager/database/fusedDownload/FusedDownload.kt +++ b/app/src/main/java/foundation/e/apps/manager/database/fusedDownload/FusedDownload.kt @@ -23,5 +23,6 @@ data class FusedDownload( val offerType: Int = -1, val isFree: Boolean = true, val appSize: Long = 0, - var files: List = mutableListOf() + var files: List = mutableListOf(), + var signature: String = String() ) diff --git a/app/src/main/java/foundation/e/apps/manager/download/DownloadManagerUtils.kt b/app/src/main/java/foundation/e/apps/manager/download/DownloadManagerUtils.kt index 13b450fb75d3bdc47381810955f201fc7678c438..c5a949a87fc41a71944a3dbabe36ca3b6492ff36 100644 --- a/app/src/main/java/foundation/e/apps/manager/download/DownloadManagerUtils.kt +++ b/app/src/main/java/foundation/e/apps/manager/download/DownloadManagerUtils.kt @@ -20,7 +20,9 @@ package foundation.e.apps.manager.download import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.manager.database.fusedDownload.FusedDownload import foundation.e.apps.manager.fused.FusedManagerRepository +import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.Status import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope @@ -59,7 +61,7 @@ class DownloadManagerUtils @Inject constructor( fusedManagerRepository.updateFusedDownload(fusedDownload) val downloaded = fusedDownload.downloadIdMap.values.filter { it }.size Timber.d("===> updateDownloadStatus: ${fusedDownload.name}: $downloadId: $downloaded/${fusedDownload.downloadIdMap.size}") - if (downloaded == fusedDownload.downloadIdMap.size) { + if (downloaded == fusedDownload.downloadIdMap.size && checkCleanApkSignatureOK(fusedDownload)) { fusedManagerRepository.moveOBBFileToOBBDirectory(fusedDownload) fusedDownload.status = Status.DOWNLOADED fusedManagerRepository.updateFusedDownload(fusedDownload) @@ -68,4 +70,19 @@ class DownloadManagerUtils @Inject constructor( } } } + + private suspend fun checkCleanApkSignatureOK(fusedDownload: FusedDownload): Boolean { + if (fusedDownload.origin != Origin.CLEANAPK || fusedManagerRepository.isFdroidApplicationSigned( + context, + fusedDownload + ) + ) { + Timber.d("Apk signature is OK") + return true + } + fusedDownload.status = Status.INSTALLATION_ISSUE + fusedManagerRepository.updateFusedDownload(fusedDownload) + Timber.d("CleanApk signature is Wrong!") + return false + } } diff --git a/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerImpl.kt b/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerImpl.kt index cc4780ba8ad8d6aebcd2bbd0899b64b378fa63b1..90161c055910cdd315e64128cf7bd43842b40848 100644 --- a/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerImpl.kt +++ b/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerImpl.kt @@ -248,6 +248,9 @@ class FusedManagerImpl @Inject constructor( } } + fun getBaseApkPath(fusedDownload: FusedDownload) = + "$cacheDir/${fusedDownload.packageName}/${fusedDownload.packageName}_1.apk" + suspend fun installationIssue(fusedDownload: FusedDownload) { flushOldDownload(fusedDownload.packageName) fusedDownload.status = Status.INSTALLATION_ISSUE diff --git a/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerRepository.kt b/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerRepository.kt index 6ee2081a7f4647d0f75863b8d967a27f02f8bafc..5830f4226fc5cfaf1b412923541c68edeb05adab 100644 --- a/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerRepository.kt +++ b/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerRepository.kt @@ -1,9 +1,11 @@ package foundation.e.apps.manager.fused +import android.content.Context import android.os.Build import androidx.annotation.RequiresApi import androidx.lifecycle.LiveData import androidx.lifecycle.asFlow +import foundation.e.apps.api.fdroid.FdroidRepository import foundation.e.apps.manager.database.fusedDownload.FusedDownload import foundation.e.apps.utils.enums.Status import kotlinx.coroutines.flow.Flow @@ -12,7 +14,8 @@ import javax.inject.Singleton @Singleton class FusedManagerRepository @Inject constructor( - private val fusedManagerImpl: FusedManagerImpl + private val fusedManagerImpl: FusedManagerImpl, + private val fdroidRepository: FdroidRepository ) { @RequiresApi(Build.VERSION_CODES.O) @@ -86,4 +89,9 @@ class FusedManagerRepository @Inject constructor( fun validateFusedDownload(fusedDownload: FusedDownload) = fusedDownload.packageName.isNotEmpty() && fusedDownload.downloadURLList.isNotEmpty() + + suspend fun isFdroidApplicationSigned(context: Context, fusedDownload: FusedDownload): Boolean { + val apkFilePath = fusedManagerImpl.getBaseApkPath(fusedDownload) + return fdroidRepository.isFdroidApplicationSigned(context, fusedDownload.packageName, apkFilePath, fusedDownload.signature) + } } diff --git a/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt b/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt index 89f05057476478daa906a81ae03c8b2085209435..ace193cfd5b74f3658ddb23b4720de1227bec951 100644 --- a/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt +++ b/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt @@ -18,14 +18,23 @@ package foundation.e.apps import android.content.Context +import com.aurora.gplayapi.Constants +import com.aurora.gplayapi.data.models.App +import com.aurora.gplayapi.data.models.AuthData import foundation.e.apps.api.cleanapk.CleanAPKRepository import foundation.e.apps.api.fused.FusedAPIImpl import foundation.e.apps.api.fused.data.FusedApp +import foundation.e.apps.api.fused.data.FusedHome import foundation.e.apps.api.gplay.GPlayAPIRepository import foundation.e.apps.manager.pkg.PkgManagerModule +import foundation.e.apps.utils.enums.FilterLevel +import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.modules.PWAManagerModule import foundation.e.apps.utils.modules.PreferenceManagerModule +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before @@ -35,6 +44,7 @@ import org.mockito.Mockito import org.mockito.MockitoAnnotations import org.mockito.kotlin.eq +@OptIn(ExperimentalCoroutinesApi::class) class FusedApiImplTest { private lateinit var fusedAPIImpl: FusedAPIImpl @@ -272,4 +282,281 @@ class FusedApiImplTest { val isAppStatusUpdated = fusedAPIImpl.isAnyAppInstallStatusChanged(oldAppList) assertFalse("hasInstallStatusUpdated", isAppStatusUpdated) } + + @Test + fun isHomeDataUpdated() { + val oldAppList = mutableListOf( + FusedApp( + _id = "111", + status = Status.INSTALLATION_ISSUE, + name = "Demo One", + package_name = "foundation.e.demoone", + latest_version_code = 123 + ), + FusedApp( + _id = "112", + status = Status.INSTALLED, + name = "Demo Two", + package_name = "foundation.e.demotwo", + latest_version_code = 123 + ), + FusedApp( + _id = "113", + status = Status.UNAVAILABLE, + name = "Demo Three", + package_name = "foundation.e.demothree", + latest_version_code = 123 + ) + ) + val newAppList = mutableListOf( + FusedApp( + _id = "111", + status = Status.INSTALLATION_ISSUE, + name = "Demo One", + package_name = "foundation.e.demoone", + latest_version_code = 123 + ), + FusedApp( + _id = "112", + status = Status.UNAVAILABLE, + name = "Demo Two", + package_name = "foundation.e.demotwo", + latest_version_code = 123 + ), + FusedApp( + _id = "113", + status = Status.UNAVAILABLE, + name = "Demo Three", + package_name = "foundation.e.demothree", + latest_version_code = 123 + ) + ) + val oldHomeData = + listOf(FusedHome("Top Free Apps", oldAppList), FusedHome("Top Free Games", oldAppList)) + var newHomeData = + listOf(FusedHome("Top Free Apps", oldAppList), FusedHome("Top Free Games", oldAppList)) + var isHomeDataUpdated = fusedAPIImpl.isHomeDataUpdated(newHomeData, oldHomeData) + assertFalse("isHomeDataUpdated/NO", isHomeDataUpdated) + newHomeData = + listOf(FusedHome("Top Free Apps", oldAppList), FusedHome("Top Free Games", newAppList)) + isHomeDataUpdated = fusedAPIImpl.isHomeDataUpdated(newHomeData, oldHomeData) + assertTrue("isHomeDataUpdated/YES", isHomeDataUpdated) + } + + @Test + fun isHomeDataUpdatedWhenBothAreEmpty() { + val oldHomeData = listOf() + val newHomeData = listOf() + val isHomeDataUpdated = fusedAPIImpl.isHomeDataUpdated(oldHomeData, newHomeData) + assertFalse("isHomeDataUpdated", isHomeDataUpdated) + } + + @Test + fun `is home data updated when fusedapp list size is not same`() { + val oldAppList = mutableListOf(FusedApp(), FusedApp(), FusedApp()) + val newAppList = mutableListOf(FusedApp(), FusedApp()) + + val oldHomeData = + listOf(FusedHome("Top Free Apps", oldAppList), FusedHome("Top Free Games", oldAppList)) + var newHomeData = + listOf(FusedHome("Top Free Apps", oldAppList), FusedHome("Top Free Games", newAppList)) + + val isHomeDataUpdated = fusedAPIImpl.isHomeDataUpdated(newHomeData, oldHomeData) + assertTrue("isHomeDataUpdated/YES", isHomeDataUpdated) + } + + @Test + fun getFusedAppInstallationStatusWhenPWA() { + val fusedApp = FusedApp( + _id = "113", + status = Status.UNAVAILABLE, + name = "Demo Three", + package_name = "foundation.e.demothree", + latest_version_code = 123, + is_pwa = true + ) + Mockito.`when`(pwaManagerModule.getPwaStatus(fusedApp)).thenReturn(fusedApp.status) + val installationStatus = fusedAPIImpl.getFusedAppInstallationStatus(fusedApp) + assertEquals("getFusedAppInstallationStatusWhenPWA", fusedApp.status, installationStatus) + } + + @Test + fun getFusedAppInstallationStatus() { + val fusedApp = FusedApp( + _id = "113", + name = "Demo Three", + package_name = "foundation.e.demothree", + latest_version_code = 123, + ) + Mockito.`when`( + pkgManagerModule.getPackageStatus( + fusedApp.package_name, + fusedApp.latest_version_code + ) + ).thenReturn(Status.INSTALLED) + val installationStatus = fusedAPIImpl.getFusedAppInstallationStatus(fusedApp) + assertEquals("getFusedAppInstallationStatusWhenPWA", Status.INSTALLED, installationStatus) + } + + @Test + fun `getAppFilterLevel when package name is empty`() = runTest { + val fusedApp = FusedApp( + _id = "113", + name = "Demo Three", + package_name = "", + latest_version_code = 123, + ) + val authData = AuthData("e@e.email", "AtadyMsIAtadyM") + val filterLevel = fusedAPIImpl.getAppFilterLevel(fusedApp, authData) + assertEquals("getAppFilterLevel", FilterLevel.UNKNOWN, filterLevel) + } + + @Test + fun `getAppFilterLevel when app is CleanApk`() = runTest { + val fusedApp = FusedApp( + _id = "113", + name = "Demo Three", + package_name = "foundation.e.demothree", + latest_version_code = 123, + origin = Origin.CLEANAPK + ) + + val authData = AuthData("e@e.email", "AtadyMsIAtadyM") + val filterLevel = fusedAPIImpl.getAppFilterLevel(fusedApp, authData) + assertEquals("getAppFilterLevel", FilterLevel.NONE, filterLevel) + } + + @Test + fun `getAppFilterLevel when Authdata is NULL`() = runTest { + val fusedApp = FusedApp( + _id = "113", + name = "Demo Three", + package_name = "foundation.e.demothree", + latest_version_code = 123, + origin = Origin.CLEANAPK + ) + val filterLevel = fusedAPIImpl.getAppFilterLevel(fusedApp, null) + assertEquals("getAppFilterLevel", FilterLevel.NONE, filterLevel) + } + + @Test + fun `getAppFilterLevel when app is restricted and paid and no price`() = runTest { + val fusedApp = FusedApp( + _id = "113", + name = "Demo Three", + package_name = "foundation.e.demothree", + latest_version_code = 123, + origin = Origin.GPLAY, + restriction = Constants.Restriction.UNKNOWN, + isFree = false, + price = "" + ) + val authData = AuthData("e@e.email", "AtadyMsIAtadyM") + val filterLevel = fusedAPIImpl.getAppFilterLevel(fusedApp, authData) + assertEquals("getAppFilterLevel", FilterLevel.UI, filterLevel) + } + + @Test + fun `getAppFilterLevel when app is not_restricted and paid and no price`() = runTest { + val fusedApp = FusedApp( + _id = "113", + name = "Demo Three", + package_name = "foundation.e.demothree", + latest_version_code = 123, + origin = Origin.GPLAY, + restriction = Constants.Restriction.NOT_RESTRICTED, + isFree = false, + price = "" + ) + val authData = AuthData("e@e.email", "AtadyMsIAtadyM") + val filterLevel = fusedAPIImpl.getAppFilterLevel(fusedApp, authData) + assertEquals("getAppFilterLevel", FilterLevel.UI, filterLevel) + } + + @Test + fun `getAppFilterLevel when app is restricted and getAppDetails and getDownloadDetails returns success`() = + runTest { + val fusedApp = FusedApp( + _id = "113", + name = "Demo Three", + package_name = "foundation.e.demothree", + latest_version_code = 123, + origin = Origin.GPLAY, + restriction = Constants.Restriction.UNKNOWN, + isFree = true, + price = "" + ) + val authData = AuthData("e@e.email", "AtadyMsIAtadyM") + Mockito.`when`(gPlayAPIRepository.getAppDetails(fusedApp.package_name, authData)) + .thenReturn(App(fusedApp.package_name)) + + Mockito.`when`( + gPlayAPIRepository.getDownloadInfo( + fusedApp.package_name, + fusedApp.latest_version_code, + fusedApp.offer_type, + authData + ) + ).thenReturn(listOf()) + val filterLevel = fusedAPIImpl.getAppFilterLevel(fusedApp, authData) + assertEquals("getAppFilterLevel", FilterLevel.NONE, filterLevel) + } + + @Test + fun `getAppFilterLevel when app is restricted and getAppDetails throws exception`() = + runTest { + val fusedApp = FusedApp( + _id = "113", + name = "Demo Three", + package_name = "foundation.e.demothree", + latest_version_code = 123, + origin = Origin.GPLAY, + restriction = Constants.Restriction.UNKNOWN, + isFree = true, + price = "" + ) + val authData = AuthData("e@e.email", "AtadyMsIAtadyM") + Mockito.`when`(gPlayAPIRepository.getAppDetails(fusedApp.package_name, authData)) + .thenThrow(RuntimeException()) + + Mockito.`when`( + gPlayAPIRepository.getDownloadInfo( + fusedApp.package_name, + fusedApp.latest_version_code, + fusedApp.offer_type, + authData + ) + ).thenReturn(listOf()) + val filterLevel = fusedAPIImpl.getAppFilterLevel(fusedApp, authData) + assertEquals("getAppFilterLevel", FilterLevel.DATA, filterLevel) + } + + @Test + fun `getAppFilterLevel when app is restricted and getDownoadInfo throws exception`() = + runTest { + val fusedApp = FusedApp( + _id = "113", + name = "Demo Three", + package_name = "foundation.e.demothree", + latest_version_code = 123, + origin = Origin.GPLAY, + restriction = Constants.Restriction.UNKNOWN, + isFree = true, + price = "" + ) + val authData = AuthData("e@e.email", "AtadyMsIAtadyM") + Mockito.`when`(gPlayAPIRepository.getAppDetails(fusedApp.package_name, authData)) + .thenReturn(App(fusedApp.package_name)) + + Mockito.`when`( + gPlayAPIRepository.getDownloadInfo( + fusedApp.package_name, + fusedApp.latest_version_code, + fusedApp.offer_type, + authData + ) + ).thenThrow(RuntimeException()) + val filterLevel = fusedAPIImpl.getAppFilterLevel(fusedApp, authData) + assertEquals("getAppFilterLevel", FilterLevel.UI, filterLevel) + } }