diff --git a/app/src/main/java/foundation/e/apps/data/exodus/repositories/AppPrivacyInfoRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/data/exodus/repositories/AppPrivacyInfoRepositoryImpl.kt index ab32cf0c92ac071b89f52da7267821a905464512..934324205a010388d0455f1c49e6bd93e478e17b 100644 --- a/app/src/main/java/foundation/e/apps/data/exodus/repositories/AppPrivacyInfoRepositoryImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/exodus/repositories/AppPrivacyInfoRepositoryImpl.kt @@ -42,14 +42,6 @@ class AppPrivacyInfoRepositoryImpl @Inject constructor( private val trackerDao: TrackerDao ) : IAppPrivacyInfoRepository { companion object { - private const val MAX_TRACKER_SCORE = 9 - private const val MIN_TRACKER_SCORE = 0 - private const val MAX_PERMISSION_SCORE = 10 - private const val MIN_PERMISSION_SCORE = 0 - private const val THRESHOLD_OF_NON_ZERO_TRACKER_SCORE = 5 - private const val THRESHOLD_OF_NON_ZERO_PERMISSION_SCORE = 9 - private const val FACTOR_OF_PERMISSION_SCORE = 0.2 - private const val DIVIDER_OF_PERMISSION_SCORE = 2.0 private const val DATE_FORMAT = "ddMMyyyy" private const val SOURCE_FDROID = "fdroid" private const val SOURCE_GOOGLE = "google" @@ -192,29 +184,4 @@ class AppPrivacyInfoRepositoryImpl @Inject constructor( latestTrackerData.trackers.contains(it.id) }.map { it.name } } - - override fun calculatePrivacyScore(fusedApp: FusedApp): Int { - if (fusedApp.permsFromExodus == LIST_OF_NULL) { - return -1 - } - - val calculateTrackersScore = calculateTrackersScore(fusedApp.trackers.size) - val calculatePermissionsScore = calculatePermissionsScore( - countAndroidPermissions(fusedApp) - ) - return calculateTrackersScore + calculatePermissionsScore - } - - private fun countAndroidPermissions(fusedApp: FusedApp) = - fusedApp.permsFromExodus.filter { it.contains("android.permission") }.size - - private fun calculateTrackersScore(numberOfTrackers: Int): Int { - return if (numberOfTrackers > THRESHOLD_OF_NON_ZERO_TRACKER_SCORE) MIN_TRACKER_SCORE else MAX_TRACKER_SCORE - numberOfTrackers - } - - private fun calculatePermissionsScore(numberOfPermission: Int): Int { - return if (numberOfPermission > THRESHOLD_OF_NON_ZERO_PERMISSION_SCORE) MIN_PERMISSION_SCORE else round( - FACTOR_OF_PERMISSION_SCORE * ceil((MAX_PERMISSION_SCORE - numberOfPermission) / DIVIDER_OF_PERMISSION_SCORE) - ).toInt() - } } diff --git a/app/src/main/java/foundation/e/apps/data/exodus/repositories/IAppPrivacyInfoRepository.kt b/app/src/main/java/foundation/e/apps/data/exodus/repositories/IAppPrivacyInfoRepository.kt index 213c96cf09fc37fc55788db523bbfd773f5065e4..6f4e9d82cddb8b8e0bc0b054f131f024c2991870 100644 --- a/app/src/main/java/foundation/e/apps/data/exodus/repositories/IAppPrivacyInfoRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/exodus/repositories/IAppPrivacyInfoRepository.kt @@ -6,5 +6,4 @@ import foundation.e.apps.data.fused.data.FusedApp interface IAppPrivacyInfoRepository { suspend fun getAppPrivacyInfo(fusedApp: FusedApp, appHandle: String): Result - fun calculatePrivacyScore(fusedApp: FusedApp): Int } diff --git a/app/src/main/java/foundation/e/apps/data/exodus/repositories/PrivacyScoreRepository.kt b/app/src/main/java/foundation/e/apps/data/exodus/repositories/PrivacyScoreRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..347eb2ead25974215e40fb784adaed53a67eb453 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/exodus/repositories/PrivacyScoreRepository.kt @@ -0,0 +1,26 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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.data.exodus.repositories + +import foundation.e.apps.data.fused.data.FusedApp + +interface PrivacyScoreRepository { + + fun calculatePrivacyScore(fusedApp: FusedApp): Int +} diff --git a/app/src/main/java/foundation/e/apps/data/exodus/repositories/PrivacyScoreRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/data/exodus/repositories/PrivacyScoreRepositoryImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..871e82640d5698bb78fd4296a0e8fdba85733d27 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/exodus/repositories/PrivacyScoreRepositoryImpl.kt @@ -0,0 +1,68 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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.data.exodus.repositories + +import foundation.e.apps.data.fused.data.FusedApp +import foundation.e.apps.di.CommonUtilsModule +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.math.ceil +import kotlin.math.round + +@Singleton +class PrivacyScoreRepositoryImpl @Inject constructor() : PrivacyScoreRepository { + + override fun calculatePrivacyScore(fusedApp: FusedApp): Int { + if (fusedApp.permsFromExodus == CommonUtilsModule.LIST_OF_NULL) { + return -1 + } + + val calculateTrackersScore = calculateTrackersScore(fusedApp.trackers.size) + val calculatePermissionsScore = calculatePermissionsScore( + countAndroidPermissions(fusedApp) + ) + return calculateTrackersScore + calculatePermissionsScore + } + + private fun calculateTrackersScore(numberOfTrackers: Int): Int { + return if (numberOfTrackers > THRESHOLD_OF_NON_ZERO_TRACKER_SCORE) MIN_TRACKER_SCORE else MAX_TRACKER_SCORE - numberOfTrackers + } + + private fun countAndroidPermissions(fusedApp: FusedApp) = + fusedApp.permsFromExodus.filter { it.contains("android.permission") }.size + + private fun calculatePermissionsScore(numberOfPermission: Int): Int { + return if (numberOfPermission > THRESHOLD_OF_NON_ZERO_PERMISSION_SCORE) MIN_PERMISSION_SCORE else round( + FACTOR_OF_PERMISSION_SCORE * ceil((MAX_PERMISSION_SCORE - numberOfPermission) / DIVIDER_OF_PERMISSION_SCORE) + ).toInt() + } + + // please do not put in the top of the class, as it can break the privacy calculation source code link. + // for more info: https://gitlab.e.foundation/e/os/backlog/-/issues/1353 + companion object { + private const val MAX_TRACKER_SCORE = 9 + private const val MIN_TRACKER_SCORE = 0 + private const val MAX_PERMISSION_SCORE = 10 + private const val MIN_PERMISSION_SCORE = 0 + private const val THRESHOLD_OF_NON_ZERO_TRACKER_SCORE = 5 + private const val THRESHOLD_OF_NON_ZERO_PERMISSION_SCORE = 9 + private const val FACTOR_OF_PERMISSION_SCORE = 0.2 + private const val DIVIDER_OF_PERMISSION_SCORE = 2.0 + } +} diff --git a/app/src/main/java/foundation/e/apps/di/RepositoryModule.kt b/app/src/main/java/foundation/e/apps/di/RepositoryModule.kt index 818511a208bb4f2f22422e59c44659bc19993eef..0a1ac2714f1438119178d953ab2d5963de8c8412 100644 --- a/app/src/main/java/foundation/e/apps/di/RepositoryModule.kt +++ b/app/src/main/java/foundation/e/apps/di/RepositoryModule.kt @@ -6,6 +6,8 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import foundation.e.apps.data.exodus.repositories.AppPrivacyInfoRepositoryImpl import foundation.e.apps.data.exodus.repositories.IAppPrivacyInfoRepository +import foundation.e.apps.data.exodus.repositories.PrivacyScoreRepository +import foundation.e.apps.data.exodus.repositories.PrivacyScoreRepositoryImpl import foundation.e.apps.data.fdroid.FdroidRepository import foundation.e.apps.data.fdroid.IFdroidRepository import foundation.e.apps.data.fused.FusedApi @@ -32,4 +34,8 @@ interface RepositoryModule { @Singleton @Binds fun getFusedApi(fusedApiImpl: FusedApiImpl): FusedApi + + @Singleton + @Binds + fun getPrivacyScoreRepository(privacyScoreRepositoryImpl: PrivacyScoreRepositoryImpl): PrivacyScoreRepository } diff --git a/app/src/main/java/foundation/e/apps/ui/PrivacyInfoViewModel.kt b/app/src/main/java/foundation/e/apps/ui/PrivacyInfoViewModel.kt index 6dfd016501f523626d4821cf0b682cbe28420033..7f24fa3c561a6c3022aef389a2399c2e96fffa50 100644 --- a/app/src/main/java/foundation/e/apps/ui/PrivacyInfoViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/PrivacyInfoViewModel.kt @@ -7,12 +7,14 @@ import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.data.Result import foundation.e.apps.data.exodus.models.AppPrivacyInfo import foundation.e.apps.data.exodus.repositories.IAppPrivacyInfoRepository +import foundation.e.apps.data.exodus.repositories.PrivacyScoreRepository import foundation.e.apps.data.fused.data.FusedApp import javax.inject.Inject @HiltViewModel class PrivacyInfoViewModel @Inject constructor( private val privacyInfoRepository: IAppPrivacyInfoRepository, + private val privacyScoreRepository: PrivacyScoreRepository, ) : ViewModel() { fun getAppPrivacyInfoLiveData(fusedApp: FusedApp): LiveData> { @@ -48,7 +50,7 @@ class PrivacyInfoViewModel @Inject constructor( fun getPrivacyScore(fusedApp: FusedApp?): Int { fusedApp?.let { - return privacyInfoRepository.calculatePrivacyScore(it) + return privacyScoreRepository.calculatePrivacyScore(it) } return -1 } diff --git a/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt b/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt index 5c40b3b2f1b07758e8cd15c58a26ffc3aab73761..c7c565403d2c6fcf25d34a5d0739804b49813f57 100644 --- a/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt @@ -125,7 +125,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { companion object { private const val PRIVACY_SCORE_SOURCE_CODE_URL = - "https://gitlab.e.foundation/e/os/apps/-/blob/main/app/src/main/java/foundation/e/apps/data/exodus/repositories/AppPrivacyInfoRepositoryImpl.kt#L196" + "https://gitlab.e.foundation/e/os/apps/-/blob/main/app/src/main/java/foundation/e/apps/data/exodus/repositories/PrivacyScoreRepositoryImpl.kt#L31" private const val EXODUS_URL = "https://exodus-privacy.eu.org" private const val EXODUS_REPORT_URL = "https://reports.exodus-privacy.eu.org/" private const val PRIVACY_GUIDELINE_URL = "https://doc.e.foundation/privacy_score" diff --git a/app/src/test/java/foundation/e/apps/exodus/AppPrivacyInfoRepositoryImplTest.kt b/app/src/test/java/foundation/e/apps/exodus/AppPrivacyInfoRepositoryImplTest.kt index b055f4d3b62d4d6df729d0df3ec0c0fd12032814..399aee6fba6949eb47e3b78c720ccb671dddd231 100644 --- a/app/src/test/java/foundation/e/apps/exodus/AppPrivacyInfoRepositoryImplTest.kt +++ b/app/src/test/java/foundation/e/apps/exodus/AppPrivacyInfoRepositoryImplTest.kt @@ -22,7 +22,6 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import foundation.e.apps.data.enums.Status import foundation.e.apps.data.exodus.repositories.AppPrivacyInfoRepositoryImpl import foundation.e.apps.data.fused.data.FusedApp -import foundation.e.apps.di.CommonUtilsModule.LIST_OF_NULL import foundation.e.apps.util.MainCoroutineRule import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -52,7 +51,8 @@ class AppPrivacyInfoRepositoryImplTest { fun setup() { fakeExodusTrackerApi = FakeExoudsTrackerApi() fakeTrackerDao = FakeTrackerDao() - appPrivacyInfoRepository = AppPrivacyInfoRepositoryImpl(fakeExodusTrackerApi, fakeTrackerDao) + appPrivacyInfoRepository = + AppPrivacyInfoRepositoryImpl(fakeExodusTrackerApi, fakeTrackerDao) } @Test @@ -98,54 +98,4 @@ class AppPrivacyInfoRepositoryImplTest { val result = appPrivacyInfoRepository.getAppPrivacyInfo(fusedApp, fusedApp.package_name) assertEquals("getAppPrivacyInfo", 2, result.data?.trackerList?.size) } - - @Test - fun calculatePrivacyScoreWhenNoTrackers() { - val fusedApp = FusedApp( - _id = "113", - status = Status.UNAVAILABLE, - name = "Demo Three", - package_name = "a.b.c", - latest_version_code = 123, - is_pwa = true, - permsFromExodus = listOf(), - perms = listOf(), - trackers = listOf() - ) - val privacyScore = appPrivacyInfoRepository.calculatePrivacyScore(fusedApp) - assertEquals("getAppPrivacyInfo", 10, privacyScore) - } - - @Test - fun calculatePrivacyScoreWhenPermsAreNotAvailable() { - val fusedApp = FusedApp( - _id = "113", - status = Status.UNAVAILABLE, - name = "Demo Three", - package_name = "a.b.c", - latest_version_code = 123, - is_pwa = true, - perms = listOf(), - trackers = listOf() - ) - val privacyScore = appPrivacyInfoRepository.calculatePrivacyScore(fusedApp) - assertEquals("getAppPrivacyInfo", -1, privacyScore) - } - - @Test - fun calculatePrivacyScoreWhenTrackersAreNotAvailable() { - val fusedApp = FusedApp( - _id = "113", - status = Status.UNAVAILABLE, - name = "Demo Three", - package_name = "a.b.c", - latest_version_code = 123, - is_pwa = true, - permsFromExodus = listOf(), - perms = listOf(), - trackers = LIST_OF_NULL - ) - val privacyScore = appPrivacyInfoRepository.calculatePrivacyScore(fusedApp) - assertEquals("getAppPrivacyInfo", 9, privacyScore) - } } diff --git a/app/src/test/java/foundation/e/apps/exodus/PrivacyScoreRepositoryImplTest.kt b/app/src/test/java/foundation/e/apps/exodus/PrivacyScoreRepositoryImplTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..b6a8c25f86c5df732446ad8bf74b720c066fb703 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/exodus/PrivacyScoreRepositoryImplTest.kt @@ -0,0 +1,87 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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.exodus + +import foundation.e.apps.data.enums.Status +import foundation.e.apps.data.exodus.repositories.PrivacyScoreRepositoryImpl +import foundation.e.apps.data.fused.data.FusedApp +import foundation.e.apps.di.CommonUtilsModule +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +class PrivacyScoreRepositoryImplTest { + + private lateinit var privacyScoreRepository: PrivacyScoreRepositoryImpl + + @Before + fun setup() { + privacyScoreRepository = PrivacyScoreRepositoryImpl() + } + + @Test + fun calculatePrivacyScoreWhenNoTrackers() { + val fusedApp = FusedApp( + _id = "113", + status = Status.UNAVAILABLE, + name = "Demo Three", + package_name = "a.b.c", + latest_version_code = 123, + is_pwa = true, + permsFromExodus = listOf(), + perms = listOf(), + trackers = listOf() + ) + val privacyScore = privacyScoreRepository.calculatePrivacyScore(fusedApp) + Assert.assertEquals("failed to retrieve valid privacy score", 10, privacyScore) + } + + @Test + fun calculatePrivacyScoreWhenPermsAreNotAvailable() { + val fusedApp = FusedApp( + _id = "113", + status = Status.UNAVAILABLE, + name = "Demo Three", + package_name = "a.b.c", + latest_version_code = 123, + is_pwa = true, + perms = listOf(), + trackers = listOf() + ) + val privacyScore = privacyScoreRepository.calculatePrivacyScore(fusedApp) + Assert.assertEquals("failed to retrieve valid privacy score", -1, privacyScore) + } + + @Test + fun calculatePrivacyScoreWhenTrackersAreNotAvailable() { + val fusedApp = FusedApp( + _id = "113", + status = Status.UNAVAILABLE, + name = "Demo Three", + package_name = "a.b.c", + latest_version_code = 123, + is_pwa = true, + permsFromExodus = listOf(), + perms = listOf(), + trackers = CommonUtilsModule.LIST_OF_NULL + ) + val privacyScore = privacyScoreRepository.calculatePrivacyScore(fusedApp) + Assert.assertEquals("failed to retrieve valid privacy score", 9, privacyScore) + } +}