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)
+ }
}