From a1021890c1d2c5af354d1cefd2b1a68fe6b25deb Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Fri, 5 Aug 2022 20:29:31 +0600 Subject: [PATCH 1/3] apk signature checking added for cleanapk --- app/build.gradle | 3 + .../main/assets/f-droid.org-signing-key.gpg | Bin 0 -> 8022 bytes .../apps/api/cleanapk/ApkSignatureManager.kt | 100 ++++++++++++++++++ .../e/apps/api/cleanapk/RetrofitModule.kt | 6 +- .../e/apps/api/fdroid/FdroidApiInterface.kt | 3 +- .../e/apps/api/fdroid/FdroidRepository.kt | 15 ++- .../apps/api/fdroid/models/FdroidApiModel.kt | 2 +- .../e/apps/api/fused/FusedAPIImpl.kt | 1 + .../e/apps/manager/database/FusedDatabase.kt | 2 +- .../database/fusedDownload/FusedDownload.kt | 3 +- .../manager/download/DownloadManagerUtils.kt | 19 +++- .../e/apps/manager/fused/FusedManagerImpl.kt | 3 + .../manager/fused/FusedManagerRepository.kt | 10 +- 13 files changed, 159 insertions(+), 8 deletions(-) create mode 100644 app/src/main/assets/f-droid.org-signing-key.gpg create mode 100644 app/src/main/java/foundation/e/apps/api/cleanapk/ApkSignatureManager.kt diff --git a/app/build.gradle b/app/build.gradle index 6c612776e..91994bf78 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 GIT binary patch literal 8022 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

d%|U?iPH%An*!&2K)doLZ za!^m3ZqJS7ZB&}hG0hjdAXR@=QhhQ=zfVTY z{qC2p!B-MsPlD8aG8^EwNZ9~C!os<);`1q&6z$&FHFFb;F#Q-N`4Z zjVC9pHEGNGcc>^n2WXy_YB0>Rf1~o@>t`Z{QZw#C6xZsGL<@2ZU}rw{c!7b6UWJ`4 zxzXt&?;t1fi(pU2Te@Bi9bCTJZ}6Vomr9!=bWn#z6${e0?e@l+F_&Pu+HV7n`})0g z4X{khS~v)lxbDGr`T|5Y%V;er4)C&RWzeAp@ZQ%8B?XBmeit?K1{bru8J=xPD_MhQ zy;m#zA3G_=fK;Zq2#$-mg`0B}_j(fS5-_~H#Cb7C7wr-ixgq0m+?dqL{jKNzB@1XiCH?PeIO;5}aWm$`rZ|iwCuDT>7)|Z&yBkwvCHlZGgo%fci z@uNX%Y@qUnPu&RN0w&S=_o`z7D)9^V%VB&oe1Vw!Lv|H4WLg!g04B1*#YN7zsV;(w zBHhhmc@%M*q%^fG!`YOR)eqS%@|T{Jw#qkUEpsN2mR zRSH`1tj1aFyC0uAJNZH480Ajw{HfTKdElbWhV&Xz9?QWe&^9BeES8$1PWAv7j3K@t z5Ij;W-9ezU4mZck8IA8?!ctK4pH|J-{?Te@%#KZI7a0vEMTzr?J@Pn3FOfhm;}2!3 zAKh(&MmoZ4k)&S|zYbA8qluEW-DJ9>Wv?zagtR7<(4icJYNTN{>YLGM-mdO$EB0y( z%X@nk`Gvc5yZL4bC)l(BQ$9l=$M@_utt*g!E{ckZ26YMqOrDFPF10!7Ber{ao!X1p zAR?neV(7X=EcGlNVUaw1L+hx3$4V>HbJu*3nYS$=g&_N5O8k+FFE2^{0dqCM1j^7{|VzX%Dk3AkgYLQ7-UKGw+mor!X5hm=y{DIAx2pMMt zGB|iiW`kAPOrR+=RRPwfMzO%olFAB;xVJ;5C{~0L_z5ay4v~aza7!ALJ{>4w=M7-8m+a5IxuWu)Rsq!i zr&TjH03phMTK((J|NlzWFa>%+cC$vyyS!3@_#Vv8czCL@2<;nP%Fr5cWHpg}XW^PWGl@y?iE%~=F>XFU-7TZ36ByNxk1 z9hcRKRe!I$n!s9Ux~k;_Kh7aLBJ>jBI;H#Jjd;|d%S(F&hur9gr9vlo#XkWFUuXkUsk0RCpRi$Z*8Lj>b z_mdZ*K7Sh1BX-Z79;ri3eLZGE$bJ2@awXK;ljIam>fx*^^RLs8f&@ulsR literal 0 HcmV?d00001 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 000000000..f9bd54436 --- /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/cleanapk/RetrofitModule.kt b/app/src/main/java/foundation/e/apps/api/cleanapk/RetrofitModule.kt index dc6af582e..911f4affa 100644 --- a/app/src/main/java/foundation/e/apps/api/cleanapk/RetrofitModule.kt +++ b/app/src/main/java/foundation/e/apps/api/cleanapk/RetrofitModule.kt @@ -18,6 +18,7 @@ package foundation.e.apps.api.cleanapk +import android.os.Build import android.util.Log import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.dataformat.yaml.YAMLFactory @@ -161,7 +162,10 @@ object RetrofitModule { fun provideInterceptor(): Interceptor { return Interceptor { chain -> val builder = chain.request().newBuilder() - builder.header("Accept-Language", Locale.getDefault().language) + builder.header( + "User-Agent", + "Dalvik/2.1.0 (Linux; U; Android ${Build.VERSION.RELEASE};)" + ).header("Accept-Language", Locale.getDefault().language) try { return@Interceptor chain.proceed(builder.build()) } catch (e: ConnectException) { 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 d002bc084..b0e6fccf7 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 d61d71791..40fb18158 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 isFdroidApplication(packageName: String): Boolean { + return fdroidApi.getFdroidInfoForPackage(packageName).isSuccessful + } + + suspend fun isFdroidApplicationSigned(context: Context, packageName: String, apkFilePath: String, signature: String): Boolean { + if (isFdroidApplication(packageName)) { + return ApkSignatureManager.verifyFdroidSignature(context, apkFilePath, signature) + } + return false + } } diff --git a/app/src/main/java/foundation/e/apps/api/fdroid/models/FdroidApiModel.kt b/app/src/main/java/foundation/e/apps/api/fdroid/models/FdroidApiModel.kt index e797823ad..c18c96591 100644 --- a/app/src/main/java/foundation/e/apps/api/fdroid/models/FdroidApiModel.kt +++ b/app/src/main/java/foundation/e/apps/api/fdroid/models/FdroidApiModel.kt @@ -21,7 +21,7 @@ class FdroidApiModel() { var authorName: String = "" @JsonCreator - constructor(@JsonProperty("AuthorName") AuthorName: String?) : this() { + constructor(@JsonProperty(" ") AuthorName: String?) : this() { this.authorName = AuthorName ?: "" } } 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 226c26de8..26893c3d0 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 @@ -401,6 +401,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( 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 c8b4c080c..3a307ef88 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 269298dd4..0cd910a68 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 13b450fb7..c5a949a87 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 cc4780ba8..90161c055 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 6ee2081a7..5830f4226 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) + } } -- GitLab From cbf3ea53a5f23595d9ee0f72a9736c6a4d7b4224 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Thu, 11 Aug 2022 21:55:05 +0600 Subject: [PATCH 2/3] fixed some logic in fusedapiimpl added some unit test some refactoring --- .../e/apps/api/fdroid/FdroidRepository.kt | 8 +- .../e/apps/api/fused/FusedAPIImpl.kt | 53 ++-- .../foundation/e/apps/FusedApiImplTest.kt | 287 ++++++++++++++++++ 3 files changed, 327 insertions(+), 21 deletions(-) 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 40fb18158..7010aed4e 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 @@ -27,14 +27,14 @@ class FdroidRepository @Inject constructor( } } - suspend fun isFdroidApplication(packageName: String): Boolean { - return fdroidApi.getFdroidInfoForPackage(packageName).isSuccessful - } - 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 26893c3d0..04a6e1b0d 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,8 @@ class FusedAPIImpl @Inject constructor( gplayPackageResult = it.first } } - } catch (_: Exception) {} + } catch (_: Exception) { + } } getCleanapkSearchResult(query).let { /* Cleanapk always returns something, it is never null. @@ -314,7 +319,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 + ) + ) } ) } @@ -454,7 +464,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) } @@ -466,7 +477,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) } @@ -482,7 +494,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( @@ -670,6 +685,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. @@ -694,13 +714,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 } @@ -1276,7 +1291,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 } } @@ -1288,15 +1303,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 } /** @@ -1325,7 +1343,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/test/java/foundation/e/apps/FusedApiImplTest.kt b/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt index 89f050574..ace193cfd 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) + } } -- GitLab From f3b80207d70e00eaee7f003039d62f778892f9bf Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Thu, 18 Aug 2022 15:02:36 +0600 Subject: [PATCH 3/3] fixed lint issues --- .../main/java/foundation/e/apps/api/cleanapk/RetrofitModule.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/foundation/e/apps/api/cleanapk/RetrofitModule.kt b/app/src/main/java/foundation/e/apps/api/cleanapk/RetrofitModule.kt index 9b86cb6f5..6ffdd079b 100644 --- a/app/src/main/java/foundation/e/apps/api/cleanapk/RetrofitModule.kt +++ b/app/src/main/java/foundation/e/apps/api/cleanapk/RetrofitModule.kt @@ -18,7 +18,6 @@ package foundation.e.apps.api.cleanapk -import android.os.Build import android.util.Log import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.dataformat.yaml.YAMLFactory -- GitLab