Loading app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt +209 −109 Original line number Original line Diff line number Diff line Loading @@ -23,21 +23,26 @@ import android.graphics.Color import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable import android.os.Bundle import android.os.Bundle import android.text.Html import android.text.Html import android.text.format.Formatter import android.util.Log import android.util.Log import android.view.View import android.view.View import android.widget.ImageView import android.widget.ImageView import android.widget.RelativeLayout import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat import androidx.core.graphics.BlendModeColorFilterCompat import androidx.core.graphics.BlendModeColorFilterCompat import androidx.core.graphics.BlendModeCompat import androidx.core.graphics.BlendModeCompat import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.findNavController import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import coil.load import coil.load import com.google.android.material.button.MaterialButton import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar import com.google.android.material.textview.MaterialTextView import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.MainActivityViewModel import foundation.e.apps.MainActivityViewModel import foundation.e.apps.R import foundation.e.apps.R Loading @@ -46,10 +51,13 @@ import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.application.model.ApplicationScreenshotsRVAdapter import foundation.e.apps.application.model.ApplicationScreenshotsRVAdapter import foundation.e.apps.application.subFrags.ApplicationDialogFragment import foundation.e.apps.application.subFrags.ApplicationDialogFragment import foundation.e.apps.databinding.FragmentApplicationBinding import foundation.e.apps.databinding.FragmentApplicationBinding import foundation.e.apps.manager.download.data.DownloadProgress import foundation.e.apps.manager.pkg.PkgManagerModule import foundation.e.apps.manager.pkg.PkgManagerModule import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.enums.User import foundation.e.apps.utils.enums.User import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Inject @AndroidEntryPoint @AndroidEntryPoint Loading @@ -70,7 +78,8 @@ class ApplicationFragment : Fragment(R.layout.fragment_application) { private var applicationIcon: ImageView? = null private var applicationIcon: ImageView? = null companion object { companion object { private const val PRIVACY_SCORE_SOURCE_CODE_URL = "https://gitlab.e.foundation/e/apps/apps/-/blob/main/app/src/main/java/foundation/e/apps/application/ApplicationViewModel.kt#L131" private const val PRIVACY_SCORE_SOURCE_CODE_URL = "https://gitlab.e.foundation/e/apps/apps/-/blob/main/app/src/main/java/foundation/e/apps/application/ApplicationViewModel.kt#L131" private const val EXODUS_URL = "https://exodus-privacy.eu.org" private const val EXODUS_URL = "https://exodus-privacy.eu.org" private const val PRIVACY_GUIDELINE_URL = "https://doc.e.foundation/privacy_score" private const val PRIVACY_GUIDELINE_URL = "https://doc.e.foundation/privacy_score" } } Loading Loading @@ -122,113 +131,6 @@ class ApplicationFragment : Fragment(R.layout.fragment_application) { } } } } applicationViewModel.appStatus.observe(viewLifecycleOwner) { status -> val installButton = binding.downloadInclude.installButton val downloadPB = binding.downloadInclude.appInstallPB val appSize = binding.downloadInclude.appSize val fusedApp = applicationViewModel.fusedApp.value ?: FusedApp() when (status) { Status.INSTALLED -> { installButton.apply { isEnabled = true text = getString(R.string.open) setTextColor(Color.WHITE) backgroundTintList = ContextCompat.getColorStateList(view.context, R.color.colorAccent) setOnClickListener { startActivity(pkgManagerModule.getLaunchIntent(fusedApp.package_name)) } } } Status.UPDATABLE -> { installButton.apply { text = getString(R.string.update) setTextColor(Color.WHITE) backgroundTintList = ContextCompat.getColorStateList(view.context, R.color.colorAccent) setOnClickListener { applicationIcon?.let { mainActivityViewModel.getApplication(fusedApp, it) } } } downloadPB.visibility = View.GONE appSize.visibility = View.VISIBLE } Status.UNAVAILABLE -> { installButton.apply { text = getString(R.string.install) setOnClickListener { applicationIcon?.let { mainActivityViewModel.getApplication(fusedApp, it) } } } downloadPB.visibility = View.GONE appSize.visibility = View.VISIBLE } Status.QUEUED -> { installButton.apply { text = getString(R.string.cancel) setOnClickListener { mainActivityViewModel.cancelDownload(fusedApp) } } } Status.DOWNLOADING -> { installButton.apply { text = getString(R.string.cancel) setOnClickListener { mainActivityViewModel.cancelDownload(fusedApp) } } downloadPB.visibility = View.VISIBLE appSize.visibility = View.GONE applicationViewModel.downloadProgress.observe(viewLifecycleOwner) { downloadPB.max = it.totalSizeBytes.values.sum().toInt() downloadPB.progress = it.bytesDownloadedSoFar.values.sum().toInt() } } Status.INSTALLING, Status.UNINSTALLING -> { installButton.isEnabled = false downloadPB.visibility = View.GONE appSize.visibility = View.VISIBLE } Status.BLOCKED -> { installButton.setOnClickListener { val errorMsg = when ( User.valueOf( mainActivityViewModel.userType.value ?: User.UNAVAILABLE.name ) ) { User.ANONYMOUS, User.UNAVAILABLE -> getString(R.string.install_blocked_anonymous) User.GOOGLE -> getString(R.string.install_blocked_google) } if (errorMsg.isNotBlank()) { Snackbar.make(view, errorMsg, Snackbar.LENGTH_SHORT).show() } } } Status.INSTALLATION_ISSUE -> { installButton.apply { text = getString(R.string.retry) setOnClickListener { applicationIcon?.let { mainActivityViewModel.getApplication(fusedApp, it) } } } downloadPB.visibility = View.GONE appSize.visibility = View.VISIBLE } else -> { Log.d(TAG, "Unknown status: $status") } } } applicationViewModel.fusedApp.observe(viewLifecycleOwner) { applicationViewModel.fusedApp.observe(viewLifecycleOwner) { if (applicationViewModel.appStatus.value == null) { if (applicationViewModel.appStatus.value == null) { applicationViewModel.appStatus.value = it.status applicationViewModel.appStatus.value = it.status Loading Loading @@ -342,11 +244,209 @@ class ApplicationFragment : Fragment(R.layout.fragment_application) { ).show(childFragmentManager, TAG) ).show(childFragmentManager, TAG) } } } } observeDownloadStatus(view) fetchAppTracker() fetchAppTracker() } } } } private fun observeDownloadStatus(view: View) { applicationViewModel.appStatus.observe(viewLifecycleOwner) { status -> val installButton = binding.downloadInclude.installButton val downloadPB = binding.downloadInclude.progressLayout val appSize = binding.downloadInclude.appSize val fusedApp = applicationViewModel.fusedApp.value ?: FusedApp() when (status) { Status.INSTALLED -> handleInstalled(installButton, view, fusedApp) Status.UPDATABLE -> handleUpdatable( installButton, view, fusedApp, downloadPB, appSize ) Status.UNAVAILABLE -> handleUnavaiable(installButton, fusedApp, downloadPB, appSize) Status.QUEUED -> handleQueued(installButton, fusedApp) Status.DOWNLOADING -> handleDownloading( installButton, fusedApp, downloadPB, appSize ) Status.INSTALLING, Status.UNINSTALLING -> handleInstallingUninstalling( installButton, downloadPB, appSize ) Status.BLOCKED -> handleBlocked(installButton, view) Status.INSTALLATION_ISSUE -> handleInstallingIssue( installButton, fusedApp, downloadPB, appSize ) else -> { Log.d(TAG, "Unknown status: $status") } } } } private fun handleInstallingIssue( installButton: MaterialButton, fusedApp: FusedApp, downloadPB: RelativeLayout, appSize: MaterialTextView ) { installButton.apply { text = getString(R.string.retry) setOnClickListener { applicationIcon?.let { mainActivityViewModel.getApplication(fusedApp, it) } } } downloadPB.visibility = View.GONE appSize.visibility = View.VISIBLE } private fun handleBlocked( installButton: MaterialButton, view: View ) { installButton.setOnClickListener { val errorMsg = when ( User.valueOf( mainActivityViewModel.userType.value ?: User.UNAVAILABLE.name ) ) { User.ANONYMOUS, User.UNAVAILABLE -> getString(R.string.install_blocked_anonymous) User.GOOGLE -> getString(R.string.install_blocked_google) } if (errorMsg.isNotBlank()) { Snackbar.make(view, errorMsg, Snackbar.LENGTH_SHORT).show() } } } private fun handleInstallingUninstalling( installButton: MaterialButton, downloadPB: RelativeLayout, appSize: MaterialTextView ) { installButton.isEnabled = false downloadPB.visibility = View.GONE appSize.visibility = View.VISIBLE } private fun handleDownloading( installButton: MaterialButton, fusedApp: FusedApp, downloadPB: RelativeLayout, appSize: MaterialTextView ) { installButton.apply { text = getString(R.string.cancel) setOnClickListener { mainActivityViewModel.cancelDownload(fusedApp) } } downloadPB.visibility = View.VISIBLE appSize.visibility = View.GONE applicationViewModel.downloadProgress.observe(viewLifecycleOwner) { lifecycleScope.launch(Dispatchers.Main) { updateProgress(it) } } } private fun handleQueued( installButton: MaterialButton, fusedApp: FusedApp ) { installButton.apply { text = getString(R.string.cancel) setOnClickListener { mainActivityViewModel.cancelDownload(fusedApp) } } } private fun handleUnavaiable( installButton: MaterialButton, fusedApp: FusedApp, downloadPB: RelativeLayout, appSize: MaterialTextView ) { installButton.apply { text = getString(R.string.install) setOnClickListener { applicationIcon?.let { mainActivityViewModel.getApplication(fusedApp, it) } } } downloadPB.visibility = View.GONE appSize.visibility = View.VISIBLE } private fun handleUpdatable( installButton: MaterialButton, view: View, fusedApp: FusedApp, downloadPB: RelativeLayout, appSize: MaterialTextView ) { installButton.apply { text = getString(R.string.update) setTextColor(Color.WHITE) backgroundTintList = ContextCompat.getColorStateList(view.context, R.color.colorAccent) setOnClickListener { applicationIcon?.let { mainActivityViewModel.getApplication(fusedApp, it) } } } downloadPB.visibility = View.GONE appSize.visibility = View.VISIBLE } private fun handleInstalled( installButton: MaterialButton, view: View, fusedApp: FusedApp ) { installButton.apply { isEnabled = true text = getString(R.string.open) setTextColor(Color.WHITE) backgroundTintList = ContextCompat.getColorStateList(view.context, R.color.colorAccent) setOnClickListener { startActivity(pkgManagerModule.getLaunchIntent(fusedApp.package_name)) } } } private suspend fun updateProgress( downloadProgress: DownloadProgress, ) { val progressResult = applicationViewModel.calculateProgress(downloadProgress) if (progressResult.first < 1) { return } val downloadedSize = "${ Formatter.formatFileSize(requireContext(), progressResult.second).substringBefore(" MB") }/${Formatter.formatFileSize(requireContext(), progressResult.first)}" val progressPercentage = ((progressResult.second / progressResult.first.toDouble()) * 100f).toInt() binding.downloadInclude.appInstallPB.progress = progressPercentage binding.downloadInclude.percentage.text = "$progressPercentage%" binding.downloadInclude.downloadedSize.text = downloadedSize } private fun getPermissionListString(): String { private fun getPermissionListString(): String { var permission = var permission = applicationViewModel.transformPermsToString() applicationViewModel.transformPermsToString() Loading app/src/main/java/foundation/e/apps/application/ApplicationViewModel.kt +17 −0 Original line number Original line Diff line number Diff line Loading @@ -31,7 +31,9 @@ import foundation.e.apps.api.exodus.models.AppPrivacyInfo import foundation.e.apps.api.exodus.repositories.IAppPrivacyInfoRepository import foundation.e.apps.api.exodus.repositories.IAppPrivacyInfoRepository import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.manager.download.data.DownloadProgress import foundation.e.apps.manager.download.data.DownloadProgressLD import foundation.e.apps.manager.download.data.DownloadProgressLD import foundation.e.apps.manager.fused.FusedManagerRepository import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.enums.Status import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers Loading @@ -44,6 +46,7 @@ import kotlin.math.round class ApplicationViewModel @Inject constructor( class ApplicationViewModel @Inject constructor( downloadProgressLD: DownloadProgressLD, downloadProgressLD: DownloadProgressLD, private val fusedAPIRepository: FusedAPIRepository, private val fusedAPIRepository: FusedAPIRepository, private val fusedManagerRepository: FusedManagerRepository, private val appPrivacyInfoRepository: IAppPrivacyInfoRepository private val appPrivacyInfoRepository: IAppPrivacyInfoRepository ) : ViewModel() { ) : ViewModel() { Loading Loading @@ -157,4 +160,18 @@ class ApplicationViewModel @Inject constructor( private fun calculatePermissionsScore(numberOfPermission: Int): Int { private fun calculatePermissionsScore(numberOfPermission: Int): Int { return if (numberOfPermission > 9) 0 else round(0.2 * ceil((10 - numberOfPermission) / 2.0)).toInt() return if (numberOfPermission > 9) 0 else round(0.2 * ceil((10 - numberOfPermission) / 2.0)).toInt() } } suspend fun calculateProgress(progress: DownloadProgress): Pair<Long, Long> { fusedApp.value?.let { app -> val appDownload = fusedManagerRepository.getDownloadList().single { it.id.contentEquals(app._id) } val downloadingMap = progress.totalSizeBytes.filter { item -> appDownload.downloadIdMap.keys.contains(item.key) } val totalSizeBytes = downloadingMap.values.sum() val downloadedSoFar = progress.bytesDownloadedSoFar.filter { item -> appDownload.downloadIdMap.keys.contains(item.key) }.values.sum() return Pair(totalSizeBytes, downloadedSoFar) } return Pair(1, 0) } } } app/src/main/java/foundation/e/apps/manager/download/data/DownloadProgressLD.kt +23 −6 Original line number Original line Diff line number Diff line Loading @@ -2,10 +2,12 @@ package foundation.e.apps.manager.download.data import android.app.DownloadManager import android.app.DownloadManager import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData import foundation.e.apps.manager.fused.FusedManagerRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Inject Loading @@ -14,6 +16,7 @@ import kotlin.coroutines.CoroutineContext class DownloadProgressLD @Inject constructor( class DownloadProgressLD @Inject constructor( private val downloadManager: DownloadManager, private val downloadManager: DownloadManager, private val downloadManagerQuery: DownloadManager.Query, private val downloadManagerQuery: DownloadManager.Query, private val fusedManagerRepository: FusedManagerRepository ) : LiveData<DownloadProgress>(), CoroutineScope { ) : LiveData<DownloadProgress>(), CoroutineScope { private val job = Job() private val job = Job() Loading @@ -25,10 +28,20 @@ class DownloadProgressLD @Inject constructor( override fun onActive() { override fun onActive() { super.onActive() super.onActive() launch { launch { while (isActive && downloadId.isNotEmpty()) { downloadManager.query(downloadManagerQuery.setFilterById(*downloadId.toLongArray())) while (isActive) { val downloads = fusedManagerRepository.getDownloadList() val downloadingList = downloads.map { it.downloadIdMap }.filter { it.values.contains(false) } val downloadingIds = mutableListOf<Long>() downloadingList.forEach { downloadingIds.addAll(it.keys) } if (downloadingIds.isEmpty()) { delay(500) continue } downloadManager.query(downloadManagerQuery.setFilterById(*downloadingIds.toLongArray())) .use { cursor -> .use { cursor -> if (cursor.moveToFirst()) { cursor.moveToFirst() while (!cursor.isAfterLast) { val id = val id = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID)) cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID)) val status = val status = Loading @@ -51,16 +64,20 @@ class DownloadProgressLD @Inject constructor( } } downloadProgress.status[id] = downloadProgress.status[id] = status == DownloadManager.STATUS_SUCCESSFUL status == DownloadManager.STATUS_SUCCESSFUL || status == DownloadManager.STATUS_FAILED if (downloadingIds.size == cursor.count) { postValue(downloadProgress) postValue(downloadProgress) } if (downloadProgress.status.all { it.value }) { if (downloadingIds.isEmpty()) { clearDownload() clearDownload() cancel() cancel() } } cursor.moveToNext() } } } } delay(1000) } } } } } } Loading app/src/main/res/layout/fragment_application_download.xml +50 −11 Original line number Original line Diff line number Diff line Loading @@ -16,23 +16,55 @@ ~ along with this program. If not, see <https://www.gnu.org/licenses/>. ~ along with this program. If not, see <https://www.gnu.org/licenses/>. --> --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_height="wrap_content" xmlns:tools="http://schemas.android.com/tools" android:layout_marginStart="20dp" android:layout_marginStart="20dp" android:layout_marginEnd="20dp" android:layout_marginEnd="20dp" android:gravity="end" android:gravity="end" android:orientation="horizontal"> android:orientation="horizontal"> <RelativeLayout android:id="@+id/progressLayout" android:layout_width="0dp" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="@id/installButton" app:layout_constraintTop_toTopOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@+id/installButton"> <com.google.android.material.textview.MaterialTextView android:id="@+id/downloadedSize" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="@color/app_info_text_color_grey" android:textSize="15sp" tools:text="18/23 mib"/> <com.google.android.material.textview.MaterialTextView android:id="@+id/percentage" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="@color/app_info_text_color_grey" android:textSize="15sp" android:layout_marginStart="10dp" android:layout_toEndOf="@id/downloadedSize" tools:text="75%"/> <com.google.android.material.progressindicator.LinearProgressIndicator <com.google.android.material.progressindicator.LinearProgressIndicator android:id="@+id/appInstallPB" android:id="@+id/appInstallPB" android:layout_width="match_parent" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_height="wrap_content" android:max="100" android:layout_gravity="center" android:layout_gravity="center" android:paddingStart="10dp" android:paddingEnd="10dp" android:paddingEnd="10dp" android:visibility="gone" /> android:visibility="visible" android:layout_below="@+id/downloadedSize" /> </RelativeLayout> <com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView android:id="@+id/appSize" android:id="@+id/appSize" Loading @@ -40,10 +72,16 @@ android:layout_height="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="20dp" android:layout_marginEnd="20dp" android:textColor="@color/app_info_text_color_grey" android:textColor="@color/app_info_text_color_grey" android:textSize="15sp" /> android:textSize="15sp" app:layout_constraintRight_toLeftOf="@+id/installButton" app:layout_constraintTop_toTopOf="@+id/installButton" app:layout_constraintBottom_toBottomOf="@+id/installButton" /> <com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton android:id="@+id/installButton" android:id="@+id/installButton" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" style="@style/InstallButtonStyle" style="@style/InstallButtonStyle" android:layout_width="120dp" android:layout_width="120dp" android:layout_height="43dp" android:layout_height="43dp" Loading @@ -51,6 +89,7 @@ android:textAllCaps="false" android:textAllCaps="false" android:textSize="18sp" android:textSize="18sp" app:autoSizeTextType="uniform" app:autoSizeTextType="uniform" app:cornerRadius="4dp" /> app:cornerRadius="4dp" /> </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout> No newline at end of file No newline at end of file Loading
app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt +209 −109 Original line number Original line Diff line number Diff line Loading @@ -23,21 +23,26 @@ import android.graphics.Color import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable import android.os.Bundle import android.os.Bundle import android.text.Html import android.text.Html import android.text.format.Formatter import android.util.Log import android.util.Log import android.view.View import android.view.View import android.widget.ImageView import android.widget.ImageView import android.widget.RelativeLayout import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat import androidx.core.graphics.BlendModeColorFilterCompat import androidx.core.graphics.BlendModeColorFilterCompat import androidx.core.graphics.BlendModeCompat import androidx.core.graphics.BlendModeCompat import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.findNavController import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import coil.load import coil.load import com.google.android.material.button.MaterialButton import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar import com.google.android.material.textview.MaterialTextView import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.MainActivityViewModel import foundation.e.apps.MainActivityViewModel import foundation.e.apps.R import foundation.e.apps.R Loading @@ -46,10 +51,13 @@ import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.application.model.ApplicationScreenshotsRVAdapter import foundation.e.apps.application.model.ApplicationScreenshotsRVAdapter import foundation.e.apps.application.subFrags.ApplicationDialogFragment import foundation.e.apps.application.subFrags.ApplicationDialogFragment import foundation.e.apps.databinding.FragmentApplicationBinding import foundation.e.apps.databinding.FragmentApplicationBinding import foundation.e.apps.manager.download.data.DownloadProgress import foundation.e.apps.manager.pkg.PkgManagerModule import foundation.e.apps.manager.pkg.PkgManagerModule import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.enums.User import foundation.e.apps.utils.enums.User import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Inject @AndroidEntryPoint @AndroidEntryPoint Loading @@ -70,7 +78,8 @@ class ApplicationFragment : Fragment(R.layout.fragment_application) { private var applicationIcon: ImageView? = null private var applicationIcon: ImageView? = null companion object { companion object { private const val PRIVACY_SCORE_SOURCE_CODE_URL = "https://gitlab.e.foundation/e/apps/apps/-/blob/main/app/src/main/java/foundation/e/apps/application/ApplicationViewModel.kt#L131" private const val PRIVACY_SCORE_SOURCE_CODE_URL = "https://gitlab.e.foundation/e/apps/apps/-/blob/main/app/src/main/java/foundation/e/apps/application/ApplicationViewModel.kt#L131" private const val EXODUS_URL = "https://exodus-privacy.eu.org" private const val EXODUS_URL = "https://exodus-privacy.eu.org" private const val PRIVACY_GUIDELINE_URL = "https://doc.e.foundation/privacy_score" private const val PRIVACY_GUIDELINE_URL = "https://doc.e.foundation/privacy_score" } } Loading Loading @@ -122,113 +131,6 @@ class ApplicationFragment : Fragment(R.layout.fragment_application) { } } } } applicationViewModel.appStatus.observe(viewLifecycleOwner) { status -> val installButton = binding.downloadInclude.installButton val downloadPB = binding.downloadInclude.appInstallPB val appSize = binding.downloadInclude.appSize val fusedApp = applicationViewModel.fusedApp.value ?: FusedApp() when (status) { Status.INSTALLED -> { installButton.apply { isEnabled = true text = getString(R.string.open) setTextColor(Color.WHITE) backgroundTintList = ContextCompat.getColorStateList(view.context, R.color.colorAccent) setOnClickListener { startActivity(pkgManagerModule.getLaunchIntent(fusedApp.package_name)) } } } Status.UPDATABLE -> { installButton.apply { text = getString(R.string.update) setTextColor(Color.WHITE) backgroundTintList = ContextCompat.getColorStateList(view.context, R.color.colorAccent) setOnClickListener { applicationIcon?.let { mainActivityViewModel.getApplication(fusedApp, it) } } } downloadPB.visibility = View.GONE appSize.visibility = View.VISIBLE } Status.UNAVAILABLE -> { installButton.apply { text = getString(R.string.install) setOnClickListener { applicationIcon?.let { mainActivityViewModel.getApplication(fusedApp, it) } } } downloadPB.visibility = View.GONE appSize.visibility = View.VISIBLE } Status.QUEUED -> { installButton.apply { text = getString(R.string.cancel) setOnClickListener { mainActivityViewModel.cancelDownload(fusedApp) } } } Status.DOWNLOADING -> { installButton.apply { text = getString(R.string.cancel) setOnClickListener { mainActivityViewModel.cancelDownload(fusedApp) } } downloadPB.visibility = View.VISIBLE appSize.visibility = View.GONE applicationViewModel.downloadProgress.observe(viewLifecycleOwner) { downloadPB.max = it.totalSizeBytes.values.sum().toInt() downloadPB.progress = it.bytesDownloadedSoFar.values.sum().toInt() } } Status.INSTALLING, Status.UNINSTALLING -> { installButton.isEnabled = false downloadPB.visibility = View.GONE appSize.visibility = View.VISIBLE } Status.BLOCKED -> { installButton.setOnClickListener { val errorMsg = when ( User.valueOf( mainActivityViewModel.userType.value ?: User.UNAVAILABLE.name ) ) { User.ANONYMOUS, User.UNAVAILABLE -> getString(R.string.install_blocked_anonymous) User.GOOGLE -> getString(R.string.install_blocked_google) } if (errorMsg.isNotBlank()) { Snackbar.make(view, errorMsg, Snackbar.LENGTH_SHORT).show() } } } Status.INSTALLATION_ISSUE -> { installButton.apply { text = getString(R.string.retry) setOnClickListener { applicationIcon?.let { mainActivityViewModel.getApplication(fusedApp, it) } } } downloadPB.visibility = View.GONE appSize.visibility = View.VISIBLE } else -> { Log.d(TAG, "Unknown status: $status") } } } applicationViewModel.fusedApp.observe(viewLifecycleOwner) { applicationViewModel.fusedApp.observe(viewLifecycleOwner) { if (applicationViewModel.appStatus.value == null) { if (applicationViewModel.appStatus.value == null) { applicationViewModel.appStatus.value = it.status applicationViewModel.appStatus.value = it.status Loading Loading @@ -342,11 +244,209 @@ class ApplicationFragment : Fragment(R.layout.fragment_application) { ).show(childFragmentManager, TAG) ).show(childFragmentManager, TAG) } } } } observeDownloadStatus(view) fetchAppTracker() fetchAppTracker() } } } } private fun observeDownloadStatus(view: View) { applicationViewModel.appStatus.observe(viewLifecycleOwner) { status -> val installButton = binding.downloadInclude.installButton val downloadPB = binding.downloadInclude.progressLayout val appSize = binding.downloadInclude.appSize val fusedApp = applicationViewModel.fusedApp.value ?: FusedApp() when (status) { Status.INSTALLED -> handleInstalled(installButton, view, fusedApp) Status.UPDATABLE -> handleUpdatable( installButton, view, fusedApp, downloadPB, appSize ) Status.UNAVAILABLE -> handleUnavaiable(installButton, fusedApp, downloadPB, appSize) Status.QUEUED -> handleQueued(installButton, fusedApp) Status.DOWNLOADING -> handleDownloading( installButton, fusedApp, downloadPB, appSize ) Status.INSTALLING, Status.UNINSTALLING -> handleInstallingUninstalling( installButton, downloadPB, appSize ) Status.BLOCKED -> handleBlocked(installButton, view) Status.INSTALLATION_ISSUE -> handleInstallingIssue( installButton, fusedApp, downloadPB, appSize ) else -> { Log.d(TAG, "Unknown status: $status") } } } } private fun handleInstallingIssue( installButton: MaterialButton, fusedApp: FusedApp, downloadPB: RelativeLayout, appSize: MaterialTextView ) { installButton.apply { text = getString(R.string.retry) setOnClickListener { applicationIcon?.let { mainActivityViewModel.getApplication(fusedApp, it) } } } downloadPB.visibility = View.GONE appSize.visibility = View.VISIBLE } private fun handleBlocked( installButton: MaterialButton, view: View ) { installButton.setOnClickListener { val errorMsg = when ( User.valueOf( mainActivityViewModel.userType.value ?: User.UNAVAILABLE.name ) ) { User.ANONYMOUS, User.UNAVAILABLE -> getString(R.string.install_blocked_anonymous) User.GOOGLE -> getString(R.string.install_blocked_google) } if (errorMsg.isNotBlank()) { Snackbar.make(view, errorMsg, Snackbar.LENGTH_SHORT).show() } } } private fun handleInstallingUninstalling( installButton: MaterialButton, downloadPB: RelativeLayout, appSize: MaterialTextView ) { installButton.isEnabled = false downloadPB.visibility = View.GONE appSize.visibility = View.VISIBLE } private fun handleDownloading( installButton: MaterialButton, fusedApp: FusedApp, downloadPB: RelativeLayout, appSize: MaterialTextView ) { installButton.apply { text = getString(R.string.cancel) setOnClickListener { mainActivityViewModel.cancelDownload(fusedApp) } } downloadPB.visibility = View.VISIBLE appSize.visibility = View.GONE applicationViewModel.downloadProgress.observe(viewLifecycleOwner) { lifecycleScope.launch(Dispatchers.Main) { updateProgress(it) } } } private fun handleQueued( installButton: MaterialButton, fusedApp: FusedApp ) { installButton.apply { text = getString(R.string.cancel) setOnClickListener { mainActivityViewModel.cancelDownload(fusedApp) } } } private fun handleUnavaiable( installButton: MaterialButton, fusedApp: FusedApp, downloadPB: RelativeLayout, appSize: MaterialTextView ) { installButton.apply { text = getString(R.string.install) setOnClickListener { applicationIcon?.let { mainActivityViewModel.getApplication(fusedApp, it) } } } downloadPB.visibility = View.GONE appSize.visibility = View.VISIBLE } private fun handleUpdatable( installButton: MaterialButton, view: View, fusedApp: FusedApp, downloadPB: RelativeLayout, appSize: MaterialTextView ) { installButton.apply { text = getString(R.string.update) setTextColor(Color.WHITE) backgroundTintList = ContextCompat.getColorStateList(view.context, R.color.colorAccent) setOnClickListener { applicationIcon?.let { mainActivityViewModel.getApplication(fusedApp, it) } } } downloadPB.visibility = View.GONE appSize.visibility = View.VISIBLE } private fun handleInstalled( installButton: MaterialButton, view: View, fusedApp: FusedApp ) { installButton.apply { isEnabled = true text = getString(R.string.open) setTextColor(Color.WHITE) backgroundTintList = ContextCompat.getColorStateList(view.context, R.color.colorAccent) setOnClickListener { startActivity(pkgManagerModule.getLaunchIntent(fusedApp.package_name)) } } } private suspend fun updateProgress( downloadProgress: DownloadProgress, ) { val progressResult = applicationViewModel.calculateProgress(downloadProgress) if (progressResult.first < 1) { return } val downloadedSize = "${ Formatter.formatFileSize(requireContext(), progressResult.second).substringBefore(" MB") }/${Formatter.formatFileSize(requireContext(), progressResult.first)}" val progressPercentage = ((progressResult.second / progressResult.first.toDouble()) * 100f).toInt() binding.downloadInclude.appInstallPB.progress = progressPercentage binding.downloadInclude.percentage.text = "$progressPercentage%" binding.downloadInclude.downloadedSize.text = downloadedSize } private fun getPermissionListString(): String { private fun getPermissionListString(): String { var permission = var permission = applicationViewModel.transformPermsToString() applicationViewModel.transformPermsToString() Loading
app/src/main/java/foundation/e/apps/application/ApplicationViewModel.kt +17 −0 Original line number Original line Diff line number Diff line Loading @@ -31,7 +31,9 @@ import foundation.e.apps.api.exodus.models.AppPrivacyInfo import foundation.e.apps.api.exodus.repositories.IAppPrivacyInfoRepository import foundation.e.apps.api.exodus.repositories.IAppPrivacyInfoRepository import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.manager.download.data.DownloadProgress import foundation.e.apps.manager.download.data.DownloadProgressLD import foundation.e.apps.manager.download.data.DownloadProgressLD import foundation.e.apps.manager.fused.FusedManagerRepository import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.enums.Status import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers Loading @@ -44,6 +46,7 @@ import kotlin.math.round class ApplicationViewModel @Inject constructor( class ApplicationViewModel @Inject constructor( downloadProgressLD: DownloadProgressLD, downloadProgressLD: DownloadProgressLD, private val fusedAPIRepository: FusedAPIRepository, private val fusedAPIRepository: FusedAPIRepository, private val fusedManagerRepository: FusedManagerRepository, private val appPrivacyInfoRepository: IAppPrivacyInfoRepository private val appPrivacyInfoRepository: IAppPrivacyInfoRepository ) : ViewModel() { ) : ViewModel() { Loading Loading @@ -157,4 +160,18 @@ class ApplicationViewModel @Inject constructor( private fun calculatePermissionsScore(numberOfPermission: Int): Int { private fun calculatePermissionsScore(numberOfPermission: Int): Int { return if (numberOfPermission > 9) 0 else round(0.2 * ceil((10 - numberOfPermission) / 2.0)).toInt() return if (numberOfPermission > 9) 0 else round(0.2 * ceil((10 - numberOfPermission) / 2.0)).toInt() } } suspend fun calculateProgress(progress: DownloadProgress): Pair<Long, Long> { fusedApp.value?.let { app -> val appDownload = fusedManagerRepository.getDownloadList().single { it.id.contentEquals(app._id) } val downloadingMap = progress.totalSizeBytes.filter { item -> appDownload.downloadIdMap.keys.contains(item.key) } val totalSizeBytes = downloadingMap.values.sum() val downloadedSoFar = progress.bytesDownloadedSoFar.filter { item -> appDownload.downloadIdMap.keys.contains(item.key) }.values.sum() return Pair(totalSizeBytes, downloadedSoFar) } return Pair(1, 0) } } }
app/src/main/java/foundation/e/apps/manager/download/data/DownloadProgressLD.kt +23 −6 Original line number Original line Diff line number Diff line Loading @@ -2,10 +2,12 @@ package foundation.e.apps.manager.download.data import android.app.DownloadManager import android.app.DownloadManager import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData import foundation.e.apps.manager.fused.FusedManagerRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Inject Loading @@ -14,6 +16,7 @@ import kotlin.coroutines.CoroutineContext class DownloadProgressLD @Inject constructor( class DownloadProgressLD @Inject constructor( private val downloadManager: DownloadManager, private val downloadManager: DownloadManager, private val downloadManagerQuery: DownloadManager.Query, private val downloadManagerQuery: DownloadManager.Query, private val fusedManagerRepository: FusedManagerRepository ) : LiveData<DownloadProgress>(), CoroutineScope { ) : LiveData<DownloadProgress>(), CoroutineScope { private val job = Job() private val job = Job() Loading @@ -25,10 +28,20 @@ class DownloadProgressLD @Inject constructor( override fun onActive() { override fun onActive() { super.onActive() super.onActive() launch { launch { while (isActive && downloadId.isNotEmpty()) { downloadManager.query(downloadManagerQuery.setFilterById(*downloadId.toLongArray())) while (isActive) { val downloads = fusedManagerRepository.getDownloadList() val downloadingList = downloads.map { it.downloadIdMap }.filter { it.values.contains(false) } val downloadingIds = mutableListOf<Long>() downloadingList.forEach { downloadingIds.addAll(it.keys) } if (downloadingIds.isEmpty()) { delay(500) continue } downloadManager.query(downloadManagerQuery.setFilterById(*downloadingIds.toLongArray())) .use { cursor -> .use { cursor -> if (cursor.moveToFirst()) { cursor.moveToFirst() while (!cursor.isAfterLast) { val id = val id = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID)) cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID)) val status = val status = Loading @@ -51,16 +64,20 @@ class DownloadProgressLD @Inject constructor( } } downloadProgress.status[id] = downloadProgress.status[id] = status == DownloadManager.STATUS_SUCCESSFUL status == DownloadManager.STATUS_SUCCESSFUL || status == DownloadManager.STATUS_FAILED if (downloadingIds.size == cursor.count) { postValue(downloadProgress) postValue(downloadProgress) } if (downloadProgress.status.all { it.value }) { if (downloadingIds.isEmpty()) { clearDownload() clearDownload() cancel() cancel() } } cursor.moveToNext() } } } } delay(1000) } } } } } } Loading
app/src/main/res/layout/fragment_application_download.xml +50 −11 Original line number Original line Diff line number Diff line Loading @@ -16,23 +16,55 @@ ~ along with this program. If not, see <https://www.gnu.org/licenses/>. ~ along with this program. If not, see <https://www.gnu.org/licenses/>. --> --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_height="wrap_content" xmlns:tools="http://schemas.android.com/tools" android:layout_marginStart="20dp" android:layout_marginStart="20dp" android:layout_marginEnd="20dp" android:layout_marginEnd="20dp" android:gravity="end" android:gravity="end" android:orientation="horizontal"> android:orientation="horizontal"> <RelativeLayout android:id="@+id/progressLayout" android:layout_width="0dp" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="@id/installButton" app:layout_constraintTop_toTopOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@+id/installButton"> <com.google.android.material.textview.MaterialTextView android:id="@+id/downloadedSize" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="@color/app_info_text_color_grey" android:textSize="15sp" tools:text="18/23 mib"/> <com.google.android.material.textview.MaterialTextView android:id="@+id/percentage" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="@color/app_info_text_color_grey" android:textSize="15sp" android:layout_marginStart="10dp" android:layout_toEndOf="@id/downloadedSize" tools:text="75%"/> <com.google.android.material.progressindicator.LinearProgressIndicator <com.google.android.material.progressindicator.LinearProgressIndicator android:id="@+id/appInstallPB" android:id="@+id/appInstallPB" android:layout_width="match_parent" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_height="wrap_content" android:max="100" android:layout_gravity="center" android:layout_gravity="center" android:paddingStart="10dp" android:paddingEnd="10dp" android:paddingEnd="10dp" android:visibility="gone" /> android:visibility="visible" android:layout_below="@+id/downloadedSize" /> </RelativeLayout> <com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView android:id="@+id/appSize" android:id="@+id/appSize" Loading @@ -40,10 +72,16 @@ android:layout_height="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="20dp" android:layout_marginEnd="20dp" android:textColor="@color/app_info_text_color_grey" android:textColor="@color/app_info_text_color_grey" android:textSize="15sp" /> android:textSize="15sp" app:layout_constraintRight_toLeftOf="@+id/installButton" app:layout_constraintTop_toTopOf="@+id/installButton" app:layout_constraintBottom_toBottomOf="@+id/installButton" /> <com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton android:id="@+id/installButton" android:id="@+id/installButton" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" style="@style/InstallButtonStyle" style="@style/InstallButtonStyle" android:layout_width="120dp" android:layout_width="120dp" android:layout_height="43dp" android:layout_height="43dp" Loading @@ -51,6 +89,7 @@ android:textAllCaps="false" android:textAllCaps="false" android:textSize="18sp" android:textSize="18sp" app:autoSizeTextType="uniform" app:autoSizeTextType="uniform" app:cornerRadius="4dp" /> app:cornerRadius="4dp" /> </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout> No newline at end of file No newline at end of file