Loading app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt +209 −109 Original line number Diff line number Diff line Loading @@ -23,21 +23,26 @@ import android.graphics.Color import android.graphics.drawable.Drawable import android.os.Bundle import android.text.Html import android.text.format.Formatter import android.util.Log import android.view.View import android.widget.ImageView import android.widget.RelativeLayout import androidx.core.content.ContextCompat import androidx.core.graphics.BlendModeColorFilterCompat import androidx.core.graphics.BlendModeCompat import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import coil.load import com.google.android.material.button.MaterialButton import com.google.android.material.snackbar.Snackbar import com.google.android.material.textview.MaterialTextView import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.MainActivityViewModel 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.subFrags.ApplicationDialogFragment 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.utils.enums.Origin import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.enums.User import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject @AndroidEntryPoint Loading @@ -70,7 +78,8 @@ class ApplicationFragment : Fragment(R.layout.fragment_application) { private var applicationIcon: ImageView? = null 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 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) { if (applicationViewModel.appStatus.value == null) { applicationViewModel.appStatus.value = it.status Loading Loading @@ -342,11 +244,209 @@ class ApplicationFragment : Fragment(R.layout.fragment_application) { ).show(childFragmentManager, TAG) } } observeDownloadStatus(view) 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 { var permission = applicationViewModel.transformPermsToString() Loading app/src/main/java/foundation/e/apps/application/ApplicationViewModel.kt +17 −0 Original line number 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.fused.FusedAPIRepository 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.fused.FusedManagerRepository import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.Status import kotlinx.coroutines.Dispatchers Loading @@ -44,6 +46,7 @@ import kotlin.math.round class ApplicationViewModel @Inject constructor( downloadProgressLD: DownloadProgressLD, private val fusedAPIRepository: FusedAPIRepository, private val fusedManagerRepository: FusedManagerRepository, private val appPrivacyInfoRepository: IAppPrivacyInfoRepository ) : ViewModel() { Loading Loading @@ -157,4 +160,18 @@ class ApplicationViewModel @Inject constructor( private fun calculatePermissionsScore(numberOfPermission: Int): Int { 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 Diff line number Diff line Loading @@ -2,10 +2,12 @@ package foundation.e.apps.manager.download.data import android.app.DownloadManager import androidx.lifecycle.LiveData import foundation.e.apps.manager.fused.FusedManagerRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import javax.inject.Inject Loading @@ -14,6 +16,7 @@ import kotlin.coroutines.CoroutineContext class DownloadProgressLD @Inject constructor( private val downloadManager: DownloadManager, private val downloadManagerQuery: DownloadManager.Query, private val fusedManagerRepository: FusedManagerRepository ) : LiveData<DownloadProgress>(), CoroutineScope { private val job = Job() Loading @@ -25,10 +28,20 @@ class DownloadProgressLD @Inject constructor( override fun onActive() { super.onActive() 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 -> if (cursor.moveToFirst()) { cursor.moveToFirst() while (!cursor.isAfterLast) { val id = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID)) val status = Loading @@ -51,16 +64,20 @@ class DownloadProgressLD @Inject constructor( } downloadProgress.status[id] = status == DownloadManager.STATUS_SUCCESSFUL status == DownloadManager.STATUS_SUCCESSFUL || status == DownloadManager.STATUS_FAILED if (downloadingIds.size == cursor.count) { postValue(downloadProgress) } if (downloadProgress.status.all { it.value }) { if (downloadingIds.isEmpty()) { clearDownload() cancel() } cursor.moveToNext() } } delay(1000) } } } Loading app/src/main/res/layout/fragment_application_download.xml +50 −11 Original line number Diff line number Diff line Loading @@ -16,23 +16,55 @@ ~ 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" android:layout_width="match_parent" android:layout_height="wrap_content" xmlns:tools="http://schemas.android.com/tools" android:layout_marginStart="20dp" android:layout_marginEnd="20dp" android:gravity="end" 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 android:id="@+id/appInstallPB" android:layout_width="match_parent" android:layout_height="wrap_content" android:max="100" android:layout_gravity="center" android:paddingStart="10dp" android:paddingEnd="10dp" android:visibility="gone" /> android:visibility="visible" android:layout_below="@+id/downloadedSize" /> </RelativeLayout> <com.google.android.material.textview.MaterialTextView android:id="@+id/appSize" Loading @@ -40,10 +72,16 @@ android:layout_height="wrap_content" android:layout_marginEnd="20dp" 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 android:id="@+id/installButton" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" style="@style/InstallButtonStyle" android:layout_width="120dp" android:layout_height="43dp" Loading @@ -51,6 +89,7 @@ android:textAllCaps="false" android:textSize="18sp" app:autoSizeTextType="uniform" app:cornerRadius="4dp" /> app:cornerRadius="4dp" /> </LinearLayout> No newline at end of file </androidx.constraintlayout.widget.ConstraintLayout> No newline at end of file Loading
app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt +209 −109 Original line number Diff line number Diff line Loading @@ -23,21 +23,26 @@ import android.graphics.Color import android.graphics.drawable.Drawable import android.os.Bundle import android.text.Html import android.text.format.Formatter import android.util.Log import android.view.View import android.widget.ImageView import android.widget.RelativeLayout import androidx.core.content.ContextCompat import androidx.core.graphics.BlendModeColorFilterCompat import androidx.core.graphics.BlendModeCompat import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import coil.load import com.google.android.material.button.MaterialButton import com.google.android.material.snackbar.Snackbar import com.google.android.material.textview.MaterialTextView import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.MainActivityViewModel 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.subFrags.ApplicationDialogFragment 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.utils.enums.Origin import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.enums.User import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject @AndroidEntryPoint Loading @@ -70,7 +78,8 @@ class ApplicationFragment : Fragment(R.layout.fragment_application) { private var applicationIcon: ImageView? = null 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 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) { if (applicationViewModel.appStatus.value == null) { applicationViewModel.appStatus.value = it.status Loading Loading @@ -342,11 +244,209 @@ class ApplicationFragment : Fragment(R.layout.fragment_application) { ).show(childFragmentManager, TAG) } } observeDownloadStatus(view) 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 { var permission = applicationViewModel.transformPermsToString() Loading
app/src/main/java/foundation/e/apps/application/ApplicationViewModel.kt +17 −0 Original line number 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.fused.FusedAPIRepository 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.fused.FusedManagerRepository import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.Status import kotlinx.coroutines.Dispatchers Loading @@ -44,6 +46,7 @@ import kotlin.math.round class ApplicationViewModel @Inject constructor( downloadProgressLD: DownloadProgressLD, private val fusedAPIRepository: FusedAPIRepository, private val fusedManagerRepository: FusedManagerRepository, private val appPrivacyInfoRepository: IAppPrivacyInfoRepository ) : ViewModel() { Loading Loading @@ -157,4 +160,18 @@ class ApplicationViewModel @Inject constructor( private fun calculatePermissionsScore(numberOfPermission: Int): Int { 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 Diff line number Diff line Loading @@ -2,10 +2,12 @@ package foundation.e.apps.manager.download.data import android.app.DownloadManager import androidx.lifecycle.LiveData import foundation.e.apps.manager.fused.FusedManagerRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import javax.inject.Inject Loading @@ -14,6 +16,7 @@ import kotlin.coroutines.CoroutineContext class DownloadProgressLD @Inject constructor( private val downloadManager: DownloadManager, private val downloadManagerQuery: DownloadManager.Query, private val fusedManagerRepository: FusedManagerRepository ) : LiveData<DownloadProgress>(), CoroutineScope { private val job = Job() Loading @@ -25,10 +28,20 @@ class DownloadProgressLD @Inject constructor( override fun onActive() { super.onActive() 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 -> if (cursor.moveToFirst()) { cursor.moveToFirst() while (!cursor.isAfterLast) { val id = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID)) val status = Loading @@ -51,16 +64,20 @@ class DownloadProgressLD @Inject constructor( } downloadProgress.status[id] = status == DownloadManager.STATUS_SUCCESSFUL status == DownloadManager.STATUS_SUCCESSFUL || status == DownloadManager.STATUS_FAILED if (downloadingIds.size == cursor.count) { postValue(downloadProgress) } if (downloadProgress.status.all { it.value }) { if (downloadingIds.isEmpty()) { clearDownload() cancel() } cursor.moveToNext() } } delay(1000) } } } Loading
app/src/main/res/layout/fragment_application_download.xml +50 −11 Original line number Diff line number Diff line Loading @@ -16,23 +16,55 @@ ~ 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" android:layout_width="match_parent" android:layout_height="wrap_content" xmlns:tools="http://schemas.android.com/tools" android:layout_marginStart="20dp" android:layout_marginEnd="20dp" android:gravity="end" 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 android:id="@+id/appInstallPB" android:layout_width="match_parent" android:layout_height="wrap_content" android:max="100" android:layout_gravity="center" android:paddingStart="10dp" android:paddingEnd="10dp" android:visibility="gone" /> android:visibility="visible" android:layout_below="@+id/downloadedSize" /> </RelativeLayout> <com.google.android.material.textview.MaterialTextView android:id="@+id/appSize" Loading @@ -40,10 +72,16 @@ android:layout_height="wrap_content" android:layout_marginEnd="20dp" 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 android:id="@+id/installButton" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" style="@style/InstallButtonStyle" android:layout_width="120dp" android:layout_height="43dp" Loading @@ -51,6 +89,7 @@ android:textAllCaps="false" android:textSize="18sp" app:autoSizeTextType="uniform" app:cornerRadius="4dp" /> app:cornerRadius="4dp" /> </LinearLayout> No newline at end of file </androidx.constraintlayout.widget.ConstraintLayout> No newline at end of file