Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1bbcc0dc authored by Fahim Salam Chowdhury's avatar Fahim Salam Chowdhury 👽
Browse files

Merge branch '6537-Add_support_to_request_exodus_report' into 'main'

6537-Add_support_to_request_exodus_report

See merge request !345
parents 0d968185 77b92318
Loading
Loading
Loading
Loading
Loading
+36 −0
Original line number Diff line number Diff line
package foundation.e.apps.ui

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import androidx.lifecycle.viewModelScope
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 kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
@@ -17,12 +20,37 @@ class PrivacyInfoViewModel @Inject constructor(
    private val privacyScoreRepository: PrivacyScoreRepository,
) : ViewModel() {

    private val singularAppPrivacyInfoLiveData: MutableLiveData<Result<AppPrivacyInfo>> =
        MutableLiveData()

    fun getAppPrivacyInfoLiveData(fusedApp: FusedApp): LiveData<Result<AppPrivacyInfo>> {
        return liveData {
            emit(fetchEmitAppPrivacyInfo(fusedApp))
        }
    }

    fun getSingularAppPrivacyInfoLiveData(fusedApp: FusedApp?): LiveData<Result<AppPrivacyInfo>> {
        fetchPrivacyInfo(fusedApp)
        return singularAppPrivacyInfoLiveData
    }

    fun refreshAppPrivacyInfo(fusedApp: FusedApp?) {
        fetchPrivacyInfo(fusedApp, true)
    }

    private fun fetchPrivacyInfo(fusedApp: FusedApp?, forced: Boolean = false) {
        fusedApp?.let {
            if (forced) {
                it.trackers = emptyList()
                it.permsFromExodus = emptyList()
            }

            viewModelScope.launch {
                singularAppPrivacyInfoLiveData.postValue(fetchEmitAppPrivacyInfo(it))
            }
        }
    }

    private suspend fun fetchEmitAppPrivacyInfo(
        fusedApp: FusedApp
    ): Result<AppPrivacyInfo> {
@@ -54,4 +82,12 @@ class PrivacyInfoViewModel @Inject constructor(
        }
        return -1
    }

    fun shouldRequestExodusReport(fusedApp: FusedApp?): Boolean {
        if (fusedApp?.isFree != true) {
            return false
        }

        return getPrivacyScore(fusedApp) < 0
    }
}
+90 −14
Original line number Diff line number Diff line
@@ -18,8 +18,10 @@

package foundation.e.apps.ui.application

import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
import android.text.Html
import android.text.format.Formatter
@@ -122,12 +124,16 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) {

    private var applicationIcon: ImageView? = null

    private var shouldReloadPrivacyInfo = false

    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/PrivacyScoreRepositoryImpl.kt"
        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"
        private const val REQUEST_EXODUS_REPORT_URL =
            "https://reports.exodus-privacy.eu.org/en/analysis/submit#"
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -333,7 +339,44 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) {
            }

            appPrivacyScoreLayout.setOnClickListener {
                if (privacyInfoViewModel.shouldRequestExodusReport(applicationViewModel.getFusedApp())) {
                    showRequestExodusReportDialog()
                    return@setOnClickListener
                }

                showPrivacyScoreCalculationLoginDialog()
            }
        }
    }

    private fun showRequestExodusReportDialog() {
        ApplicationDialogFragment(
            R.drawable.ic_lock,
            getString(R.string.request_exodus_report),
            getRequestExodusReportDialogDetailsText(),
            getString(R.string.ok),
            {
                shouldReloadPrivacyInfo = true
                openRequestExodusReportUrl()
            },
            getString(R.string.cancel)
        ).show(childFragmentManager, TAG)
    }

    private fun getRequestExodusReportDialogDetailsText() = getString(
        R.string.request_exodus_report_confirm_dialog,
        getString(R.string.ok),
        getString(R.string.app_name)
    )

    private fun openRequestExodusReportUrl() {
        val openUrlIntent = Intent(Intent.ACTION_VIEW)
        openUrlIntent.data =
            Uri.parse("${REQUEST_EXODUS_REPORT_URL}${applicationViewModel.getFusedApp()?.package_name}")
        startActivity(openUrlIntent)
    }

    private fun showPrivacyScoreCalculationLoginDialog() {
        ApplicationDialogFragment(
            R.drawable.ic_lock,
            getString(R.string.privacy_score),
@@ -345,8 +388,6 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) {
            )
        ).show(childFragmentManager, TAG)
    }
        }
    }

    private fun updateAppTitlePanel(it: FusedApp) {
        binding.titleInclude.apply {
@@ -465,6 +506,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) {
                        downloadPB,
                        appSize
                    )

                    Status.UPDATABLE -> handleUpdatable(
                        installButton,
                        view,
@@ -472,29 +514,34 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) {
                        downloadPB,
                        appSize
                    )

                    Status.UNAVAILABLE -> handleUnavaiable(
                        installButton,
                        fusedApp,
                        downloadPB,
                        appSize
                    )

                    Status.QUEUED, Status.AWAITING, Status.DOWNLOADED -> handleQueued(
                        installButton,
                        fusedApp,
                        downloadPB,
                        appSize
                    )

                    Status.DOWNLOADING -> handleDownloading(
                        installButton,
                        fusedApp,
                        downloadPB,
                        appSize
                    )

                    Status.INSTALLING -> handleInstalling(
                        installButton,
                        downloadPB,
                        appSize
                    )

                    Status.BLOCKED -> handleBlocked(installButton, view)
                    Status.INSTALLATION_ISSUE -> handleInstallingIssue(
                        installButton,
@@ -502,6 +549,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) {
                        downloadPB,
                        appSize
                    )

                    else -> {
                        Timber.d("Unknown status: $status")
                    }
@@ -537,6 +585,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) {
                User.ANONYMOUS,
                User.NO_GOOGLE,
                User.UNAVAILABLE -> getString(R.string.install_blocked_anonymous)

                User.GOOGLE -> getString(R.string.install_blocked_google)
            }
            if (errorMsg.isNotBlank()) {
@@ -605,6 +654,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) {
            text = when {
                mainActivityViewModel.checkUnsupportedApplication(fusedApp) ->
                    getString(R.string.not_available)

                fusedApp.isFree -> getString(R.string.install)
                else -> fusedApp.price
            }
@@ -773,7 +823,8 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) {
    }

    private fun fetchAppTracker(fusedApp: FusedApp) {
        privacyInfoViewModel.getAppPrivacyInfoLiveData(fusedApp).observe(viewLifecycleOwner) {
        privacyInfoViewModel.getSingularAppPrivacyInfoLiveData(fusedApp)
            .observe(viewLifecycleOwner) {
                updatePrivacyScore()
            }
    }
@@ -814,12 +865,37 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) {
            loadingBar.isVisible = !visible
        }
        binding.ratingsInclude.loadingBar.isVisible = !visible
        binding.ratingsInclude.appPrivacyScore.visibility = visibility

        togglePrivacyScoreVisibility(visible)
    }

    private fun togglePrivacyScoreVisibility(visible: Boolean) {
        var isRequestReportVisible = false
        var privacyScoreVisibility = if (visible) View.VISIBLE else View.INVISIBLE

        if (visible) {
            isRequestReportVisible =
                privacyInfoViewModel.shouldRequestExodusReport(applicationViewModel.getFusedApp())
            privacyScoreVisibility = if (isRequestReportVisible) View.INVISIBLE else View.VISIBLE
        }

        binding.ratingsInclude.appPrivacyScore.visibility = privacyScoreVisibility
        binding.ratingsInclude.requestExodusReport.isVisible = isRequestReportVisible
    }

    override fun onResume() {
        super.onResume()
        observeDownloadList()
        reloadPrivacyInfo()
    }

    private fun reloadPrivacyInfo() {
        if (shouldReloadPrivacyInfo) {
            togglePrivacyInfoVisibility(false)
            privacyInfoViewModel.refreshAppPrivacyInfo(applicationViewModel.getFusedApp())
        }

        shouldReloadPrivacyInfo = false
    }

    override fun onDestroyView() {
+30 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?><!--
  ~ Apps  Quickly and easily install Android apps onto your device!
  ~ Copyright (C) 2023  MURENA SAS
  ~
  ~ This program is free software: you can redistribute it and/or modify
  ~ it under the terms of the GNU General Public License as published by
  ~ the Free Software Foundation, either version 3 of the License, or
  ~ (at your option) any later version.
  ~
  ~ This program is distributed in the hope that it will be useful,
  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  ~ GNU General Public License for more details.
  ~
  ~ You should have received a copy of the GNU General Public License
  ~ along with this program.  If not, see <https://www.gnu.org/licenses/>.
  -->

<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <solid android:color="@android:color/transparent" />

    <corners android:radius="4dp" />

    <stroke
        android:width="1dp"
        android:color="@color/colorAccent" />

</shape>
+18 −1
Original line number Diff line number Diff line
@@ -62,10 +62,10 @@
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:drawablePadding="5sp"
                android:drawableTint="@color/colorAccent"
                android:text="@string/rating"
                android:textColor="?android:textColorPrimary"
                android:textSize="15sp"
                android:drawableTint="@color/colorAccent"
                app:drawableStartCompat="@drawable/ic_star" />
        </LinearLayout>

@@ -90,6 +90,23 @@
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="@+id/appPrivacyScore" />

            <com.google.android.material.textview.MaterialTextView
                android:id="@+id/requestExodusReport"
                android:layout_width="wrap_content"
                android:layout_height="25dp"
                android:background="@drawable/bg_button_rounded"
                android:gravity="center"
                android:padding="4dp"
                android:text="@string/request_exodus_report"
                android:textColor="@color/colorAccent"
                android:textSize="20sp"
                android:visibility="invisible"
                app:autoSizeTextType="uniform"
                app:layout_constraintBottom_toBottomOf="@+id/appPrivacyScore"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="@+id/appPrivacyScore" />

            <com.google.android.material.textview.MaterialTextView
                android:id="@+id/appPrivacyScore"
                android:layout_width="wrap_content"
+4 −1
Original line number Diff line number Diff line
@@ -224,4 +224,7 @@

    <string name="google_login_alert_message">We recommend using a dedicated Google account to:\n\n\t\u2022 mitigate micro-targeting\n\t\u2022 limit impact in case this account is restricted by Google</string>
    <string name="proceed_to_google_login">Proceed to Google login</string>

    <string name="request_exodus_report">Request Exodus report</string>
    <string name="request_exodus_report_confirm_dialog">Clicking on \"<xliff:g id="ok">%1$s</xliff:g>\" will open a tab in your browser with the app\'s package name prefilled.&lt;br /&gt;&lt;br /&gt;Click on \"Perform analysis\" to start the analysis by Exodus.&lt;br /&gt;&lt;br /&gt;When the button \"See the report\" is displayed (it can take a while depending on the app) you can close the tab and go back to the app description in <xliff:g id="app_name">%2$s</xliff:g> where you should see the Privacy Score. Sometimes Exodus can fail to analyze the app.&lt;br /&gt;&lt;br /&gt;NB: it can take up to 10 min for the score to be displayed in the app description.</string>
</resources>