UpdatesFragment: Refactor to work with new downloads logic
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
@@ -26,6 +26,7 @@ import com.aurora.extensions.getString
|
|||||||
import com.aurora.extensions.runOnUiThread
|
import com.aurora.extensions.runOnUiThread
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.State
|
import com.aurora.store.State
|
||||||
|
import com.aurora.store.data.model.DownloadStatus
|
||||||
import com.aurora.store.databinding.ViewUpdateButtonBinding
|
import com.aurora.store.databinding.ViewUpdateButtonBinding
|
||||||
|
|
||||||
class UpdateButton : RelativeLayout {
|
class UpdateButton : RelativeLayout {
|
||||||
@@ -72,12 +73,12 @@ class UpdateButton : RelativeLayout {
|
|||||||
B.btnPositive.text = getString(text)
|
B.btnPositive.text = getString(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateState(state: State) {
|
fun updateState(downloadStatus: DownloadStatus) {
|
||||||
val displayChild = when (state) {
|
val displayChild = when (downloadStatus) {
|
||||||
State.IDLE, State.CANCELED -> 0
|
DownloadStatus.QUEUED,
|
||||||
State.QUEUED -> 1
|
DownloadStatus.DOWNLOADING -> 2
|
||||||
State.PROGRESS -> 2
|
DownloadStatus.COMPLETED -> 3
|
||||||
State.COMPLETE -> 3
|
else -> 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if (B.viewFlipper.displayedChild != displayChild) {
|
if (B.viewFlipper.displayedChild != displayChild) {
|
||||||
|
|||||||
@@ -34,13 +34,17 @@ import com.airbnb.epoxy.ModelView
|
|||||||
import com.airbnb.epoxy.OnViewRecycled
|
import com.airbnb.epoxy.OnViewRecycled
|
||||||
import com.aurora.extensions.invisible
|
import com.aurora.extensions.invisible
|
||||||
import com.aurora.extensions.px
|
import com.aurora.extensions.px
|
||||||
import com.aurora.extensions.show
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.State
|
import com.aurora.store.State
|
||||||
import com.aurora.store.data.model.UpdateFile
|
import com.aurora.store.data.model.DownloadStatus
|
||||||
|
import com.aurora.store.data.room.download.Download
|
||||||
import com.aurora.store.databinding.ViewAppUpdateBinding
|
import com.aurora.store.databinding.ViewAppUpdateBinding
|
||||||
import com.aurora.store.util.CommonUtil
|
import com.aurora.store.util.CommonUtil
|
||||||
|
import com.aurora.store.util.PathUtil
|
||||||
import com.aurora.store.view.epoxy.views.BaseView
|
import com.aurora.store.view.epoxy.views.BaseView
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.io.path.pathString
|
||||||
|
|
||||||
@ModelView(
|
@ModelView(
|
||||||
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
|
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
|
||||||
@@ -72,76 +76,77 @@ class AppUpdateView : RelativeLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ModelProp
|
@ModelProp
|
||||||
fun updateFile(updateFile: UpdateFile?) {
|
fun app(app: App) {
|
||||||
if (updateFile != null) {
|
/*Inflate App details*/
|
||||||
|
with(app) {
|
||||||
/*Inflate App details*/
|
B.txtLine1.text = displayName
|
||||||
with(updateFile.app) {
|
B.imgIcon.load(iconArtwork.url) {
|
||||||
B.txtLine1.text = displayName
|
placeholder(R.drawable.bg_placeholder)
|
||||||
B.imgIcon.load(iconArtwork.url) {
|
transformations(RoundedCornersTransformation(8.px.toFloat()))
|
||||||
placeholder(R.drawable.bg_placeholder)
|
|
||||||
transformations(RoundedCornersTransformation(8.px.toFloat()))
|
|
||||||
}
|
|
||||||
|
|
||||||
B.txtLine2.text = developerName
|
|
||||||
B.txtLine3.text = ("${CommonUtil.addSiPrefix(size)} • $updatedOn")
|
|
||||||
B.txtLine4.text = ("$versionName (${versionCode})")
|
|
||||||
B.txtChangelog.text = if (changes.isNotEmpty())
|
|
||||||
HtmlCompat.fromHtml(
|
|
||||||
changes,
|
|
||||||
HtmlCompat.FROM_HTML_OPTION_USE_CSS_COLORS
|
|
||||||
)
|
|
||||||
else
|
|
||||||
context.getString(R.string.details_changelog_unavailable)
|
|
||||||
|
|
||||||
B.headerIndicator.setOnClickListener {
|
|
||||||
if (B.txtChangelog.isVisible) {
|
|
||||||
B.headerIndicator.icon = ContextCompat.getDrawable(context, R.drawable.ic_arrow_down)
|
|
||||||
B.txtChangelog.visibility = View.GONE
|
|
||||||
} else {
|
|
||||||
B.headerIndicator.icon = ContextCompat.getDrawable(context, R.drawable.ic_arrow_up)
|
|
||||||
B.txtChangelog.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*Inflate Download details*/
|
B.txtLine2.text = developerName
|
||||||
updateFile.group?.let {
|
B.txtLine3.text = ("${CommonUtil.addSiPrefix(size)} • $updatedOn")
|
||||||
when (updateFile.state) {
|
B.txtLine4.text = ("$versionName (${versionCode})")
|
||||||
State.QUEUED -> {
|
B.txtChangelog.text = if (changes.isNotEmpty())
|
||||||
B.progressDownload.progress = 0
|
HtmlCompat.fromHtml(
|
||||||
B.progressDownload.show()
|
changes,
|
||||||
B.btnAction.updateState(State.QUEUED)
|
HtmlCompat.FROM_HTML_OPTION_USE_CSS_COLORS
|
||||||
}
|
)
|
||||||
State.IDLE, State.CANCELED -> {
|
else
|
||||||
B.progressDownload.progress = 0
|
context.getString(R.string.details_changelog_unavailable)
|
||||||
B.progressDownload.invisible()
|
|
||||||
B.btnAction.updateState(State.IDLE)
|
B.headerIndicator.setOnClickListener {
|
||||||
}
|
if (B.txtChangelog.isVisible) {
|
||||||
State.PROGRESS -> {
|
B.headerIndicator.icon = ContextCompat.getDrawable(context, R.drawable.ic_arrow_down)
|
||||||
val progress = it.groupDownloadProgress
|
B.txtChangelog.visibility = View.GONE
|
||||||
if (progress > 0) {
|
} else {
|
||||||
if (progress == 100) {
|
B.headerIndicator.icon = ContextCompat.getDrawable(context, R.drawable.ic_arrow_up)
|
||||||
B.progressDownload.invisible()
|
B.txtChangelog.visibility = View.VISIBLE
|
||||||
} else {
|
|
||||||
B.progressDownload.progress = progress
|
|
||||||
B.progressDownload.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
State.COMPLETE -> {
|
|
||||||
B.progressDownload.invisible()
|
|
||||||
B.btnAction.updateState(State.COMPLETE)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ModelProp
|
@ModelProp
|
||||||
fun state(state: State?) {
|
fun download(download: Download?) {
|
||||||
state?.let {
|
if (download != null) {
|
||||||
B.btnAction.updateState(it)
|
/*Inflate Download details*/
|
||||||
|
B.btnAction.updateState(download.status)
|
||||||
|
when (download.status) {
|
||||||
|
DownloadStatus.QUEUED -> {
|
||||||
|
B.progressDownload.progress = 0
|
||||||
|
B.progressDownload.show()
|
||||||
|
}
|
||||||
|
DownloadStatus.DOWNLOADING -> {
|
||||||
|
if (download.progress > 0) {
|
||||||
|
if (download.progress == 100) {
|
||||||
|
B.progressDownload.invisible()
|
||||||
|
} else {
|
||||||
|
B.progressDownload.progress = download.progress
|
||||||
|
B.progressDownload.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DownloadStatus.COMPLETED -> {
|
||||||
|
B.progressDownload.invisible()
|
||||||
|
|
||||||
|
// It is possible that while the app was downloaded in past, files have been removed
|
||||||
|
// Check once again to reflect appropriate status
|
||||||
|
val files = File(
|
||||||
|
PathUtil.getAppDownloadDir(
|
||||||
|
context,
|
||||||
|
download.packageName,
|
||||||
|
download.versionCode
|
||||||
|
).pathString
|
||||||
|
).listFiles()
|
||||||
|
if (files.isNullOrEmpty()) B.btnAction.updateState(DownloadStatus.UNAVAILABLE)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
B.progressDownload.progress = 0
|
||||||
|
B.progressDownload.invisible()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,26 +20,21 @@
|
|||||||
package com.aurora.store.view.ui.updates
|
package com.aurora.store.view.ui.updates
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.ServiceConnection
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.IBinder
|
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.aurora.extensions.isRAndAbove
|
import com.aurora.extensions.isRAndAbove
|
||||||
import com.aurora.extensions.toast
|
import com.aurora.extensions.toast
|
||||||
import com.aurora.gplayapi.data.models.App
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.State
|
import com.aurora.store.data.room.download.Download
|
||||||
import com.aurora.store.data.downloader.getGroupId
|
|
||||||
import com.aurora.store.data.model.UpdateFile
|
|
||||||
import com.aurora.store.data.service.UpdateService
|
|
||||||
import com.aurora.store.databinding.FragmentUpdatesBinding
|
import com.aurora.store.databinding.FragmentUpdatesBinding
|
||||||
import com.aurora.store.util.PathUtil
|
import com.aurora.store.util.PathUtil
|
||||||
import com.aurora.store.util.isExternalStorageEnable
|
import com.aurora.store.util.isExternalStorageEnable
|
||||||
@@ -49,10 +44,12 @@ import com.aurora.store.view.epoxy.views.app.NoAppViewModel_
|
|||||||
import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_
|
import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_
|
||||||
import com.aurora.store.view.ui.commons.BaseFragment
|
import com.aurora.store.view.ui.commons.BaseFragment
|
||||||
import com.aurora.store.viewmodel.all.UpdatesViewModel
|
import com.aurora.store.viewmodel.all.UpdatesViewModel
|
||||||
import com.tonyodev.fetch2.AbstractFetchGroupListener
|
|
||||||
import com.tonyodev.fetch2.Download
|
|
||||||
import com.tonyodev.fetch2.FetchGroup
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import java.io.File
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.io.path.pathString
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class UpdatesFragment : BaseFragment(R.layout.fragment_updates) {
|
class UpdatesFragment : BaseFragment(R.layout.fragment_updates) {
|
||||||
@@ -67,113 +64,30 @@ class UpdatesFragment : BaseFragment(R.layout.fragment_updates) {
|
|||||||
private val startForStorageManagerResult =
|
private val startForStorageManagerResult =
|
||||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
if (isRAndAbove() && Environment.isExternalStorageManager()) {
|
if (isRAndAbove() && Environment.isExternalStorageManager()) {
|
||||||
updateSingle(app)
|
viewModel.download(app)
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.permissions_denied)
|
toast(R.string.permissions_denied)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val startForPermissions =
|
private val startForPermissions =
|
||||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { perm ->
|
registerForActivityResult(ActivityResultContracts.RequestPermission()) { perm ->
|
||||||
if (perm) updateSingle(app) else toast(R.string.permissions_denied)
|
if (perm) viewModel.download(app) else toast(R.string.permissions_denied)
|
||||||
}
|
}
|
||||||
|
|
||||||
val listOfActionsWhenServiceAttaches = ArrayList<Runnable>()
|
|
||||||
private lateinit var fetchListener: AbstractFetchGroupListener
|
|
||||||
|
|
||||||
private var updateService: UpdateService? = null
|
|
||||||
private var attachToServiceCalled = false
|
|
||||||
private var serviceConnection = object : ServiceConnection {
|
|
||||||
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
|
|
||||||
updateService = (binder as UpdateService.UpdateServiceBinder).getUpdateService()
|
|
||||||
updateService!!.registerFetchListener(fetchListener)
|
|
||||||
if (listOfActionsWhenServiceAttaches.isNotEmpty()) {
|
|
||||||
val iterator = listOfActionsWhenServiceAttaches.iterator()
|
|
||||||
while (iterator.hasNext()) {
|
|
||||||
val next = iterator.next()
|
|
||||||
next.run()
|
|
||||||
iterator.remove()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName) {
|
|
||||||
updateService = null
|
|
||||||
attachToServiceCalled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var updateFileMap: MutableMap<Int, UpdateFile> = mutableMapOf()
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
getUpdateServiceInstance()
|
|
||||||
super.onResume()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
if (updateService != null) {
|
|
||||||
updateService = null
|
|
||||||
attachToServiceCalled = false
|
|
||||||
requireContext().unbindService(serviceConnection)
|
|
||||||
}
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
if (updateService != null) {
|
|
||||||
updateService = null
|
|
||||||
attachToServiceCalled = false
|
|
||||||
requireContext().unbindService(serviceConnection)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
_binding = FragmentUpdatesBinding.bind(view)
|
_binding = FragmentUpdatesBinding.bind(view)
|
||||||
|
|
||||||
fetchListener = object : AbstractFetchGroupListener() {
|
viewModel.observe()
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
override fun onAdded(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
viewModel.updates.combine(viewModel.downloadsList) { uList, dList ->
|
||||||
viewModel.updateDownload(groupId, fetchGroup)
|
uList?.associateWith { a ->
|
||||||
}
|
dList.find { it.packageName == a.packageName && it.versionCode == a.versionCode }
|
||||||
|
|
||||||
override fun onProgress(
|
|
||||||
groupId: Int,
|
|
||||||
download: Download,
|
|
||||||
etaInMilliSeconds: Long,
|
|
||||||
downloadedBytesPerSecond: Long,
|
|
||||||
fetchGroup: FetchGroup
|
|
||||||
) {
|
|
||||||
viewModel.updateDownload(groupId, fetchGroup)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
|
||||||
if (fetchGroup.groupDownloadProgress == 100) {
|
|
||||||
viewModel.updateDownload(groupId, fetchGroup, isComplete = true)
|
|
||||||
}
|
}
|
||||||
|
}.collectLatest {
|
||||||
|
updateController(it)
|
||||||
|
binding.swipeRefreshLayout.isRefreshing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
|
||||||
viewModel.updateDownload(groupId, fetchGroup, isCancelled = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDeleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
|
||||||
viewModel.updateDownload(groupId, fetchGroup, isCancelled = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getUpdateServiceInstance()
|
|
||||||
|
|
||||||
viewModel.liveUpdateData.observe(viewLifecycleOwner) {
|
|
||||||
updateFileMap = it
|
|
||||||
updateController(updateFileMap)
|
|
||||||
binding.swipeRefreshLayout.isRefreshing = false
|
|
||||||
updateService?.liveUpdateData?.postValue(updateFileMap)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||||
@@ -183,10 +97,15 @@ class UpdatesFragment : BaseFragment(R.layout.fragment_updates) {
|
|||||||
updateController(null)
|
updateController(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateController(updateFileMap: MutableMap<Int, UpdateFile>?) {
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateController(appList: Map<App, Download?>?) {
|
||||||
binding.recycler.withModels {
|
binding.recycler.withModels {
|
||||||
setFilterDuplicates(true)
|
setFilterDuplicates(true)
|
||||||
if (updateFileMap == null) {
|
if (appList == null) {
|
||||||
for (i in 1..10) {
|
for (i in 1..10) {
|
||||||
add(
|
add(
|
||||||
AppListViewShimmerModel_()
|
AppListViewShimmerModel_()
|
||||||
@@ -194,7 +113,7 @@ class UpdatesFragment : BaseFragment(R.layout.fragment_updates) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (updateFileMap.isEmpty()) {
|
if (appList.isEmpty()) {
|
||||||
add(
|
add(
|
||||||
NoAppViewModel_()
|
NoAppViewModel_()
|
||||||
.id("no_update")
|
.id("no_update")
|
||||||
@@ -206,8 +125,8 @@ class UpdatesFragment : BaseFragment(R.layout.fragment_updates) {
|
|||||||
UpdateHeaderViewModel_()
|
UpdateHeaderViewModel_()
|
||||||
.id("header_all")
|
.id("header_all")
|
||||||
.title(
|
.title(
|
||||||
"${updateFileMap.size} " +
|
"${appList.size} " +
|
||||||
if (updateFileMap.size == 1)
|
if (appList.size == 1)
|
||||||
getString(R.string.update_available)
|
getString(R.string.update_available)
|
||||||
else
|
else
|
||||||
getString(R.string.updates_available)
|
getString(R.string.updates_available)
|
||||||
@@ -219,42 +138,46 @@ class UpdatesFragment : BaseFragment(R.layout.fragment_updates) {
|
|||||||
getString(R.string.action_update_all)
|
getString(R.string.action_update_all)
|
||||||
)
|
)
|
||||||
.click { _ ->
|
.click { _ ->
|
||||||
if (viewModel.updateAllEnqueued)
|
if (viewModel.updateAllEnqueued) {
|
||||||
cancelAll()
|
cancelAll()
|
||||||
else
|
} else {
|
||||||
updateFileMap.values.forEach { updateSingle(it.app, true) }
|
appList.keys.forEach { updateSingle(it, true) }
|
||||||
|
}
|
||||||
requestModelBuild()
|
requestModelBuild()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
updateFileMap.values.forEach { updateFile ->
|
for ((app, download) in appList) {
|
||||||
add(
|
add(
|
||||||
AppUpdateViewModel_()
|
AppUpdateViewModel_()
|
||||||
.id(updateFile.hashCode())
|
.id(app.packageName)
|
||||||
.updateFile(updateFile)
|
.app(app)
|
||||||
.click { _ ->
|
.download(download)
|
||||||
openDetailsFragment(
|
.click { _ -> openDetailsFragment(app.packageName, app) }
|
||||||
updateFile.app.packageName,
|
|
||||||
updateFile.app
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.longClick { _ ->
|
.longClick { _ ->
|
||||||
openAppMenuSheet(updateFile.app)
|
openAppMenuSheet(app)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
.positiveAction { _ -> updateSingle(updateFile.app) }
|
.positiveAction { _ -> updateSingle(app) }
|
||||||
.negativeAction { _ -> cancelSingle(updateFile.app) }
|
.negativeAction { _ -> cancelSingle(app) }
|
||||||
.installAction { _ ->
|
.installAction { _ ->
|
||||||
updateFile.group?.downloads?.let {
|
val files = File(
|
||||||
viewModel.install(
|
PathUtil.getAppDownloadDir(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
updateFile.app.packageName,
|
app.packageName,
|
||||||
it
|
app.versionCode
|
||||||
)
|
).pathString
|
||||||
|
).listFiles()
|
||||||
|
|
||||||
|
// Downloaded files are missing, trigger re-download
|
||||||
|
if (files.isNullOrEmpty()) {
|
||||||
|
updateSingle(app)
|
||||||
|
return@installAction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val apkFiles = files.filter { it.path.endsWith(".apk") }
|
||||||
|
viewModel.install(requireContext(), app.packageName, apkFiles)
|
||||||
}
|
}
|
||||||
.state(updateFile.state)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,78 +185,42 @@ class UpdatesFragment : BaseFragment(R.layout.fragment_updates) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun runInService(runnable: Runnable) {
|
|
||||||
if (updateService == null) {
|
|
||||||
listOfActionsWhenServiceAttaches.add(runnable)
|
|
||||||
getUpdateServiceInstance()
|
|
||||||
} else {
|
|
||||||
runnable.run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateSingle(app: App, updateAll: Boolean = false) {
|
private fun updateSingle(app: App, updateAll: Boolean = false) {
|
||||||
this.app = app
|
this.app = app
|
||||||
runInService {
|
viewModel.updateAllEnqueued = updateAll
|
||||||
viewModel.updateState(app.getGroupId(requireContext()), State.QUEUED)
|
|
||||||
viewModel.updateAllEnqueued = updateAll
|
|
||||||
|
|
||||||
if (PathUtil.needsStorageManagerPerm(app.fileList) ||
|
if (PathUtil.needsStorageManagerPerm(app.fileList) ||
|
||||||
requireContext().isExternalStorageEnable()
|
requireContext().isExternalStorageEnable()
|
||||||
) {
|
) {
|
||||||
if (isRAndAbove()) {
|
if (isRAndAbove()) {
|
||||||
if (!Environment.isExternalStorageManager()) {
|
if (!Environment.isExternalStorageManager()) {
|
||||||
startForStorageManagerResult.launch(
|
startForStorageManagerResult.launch(
|
||||||
Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
|
Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
updateService?.updateApp(app)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (ContextCompat.checkSelfPermission(
|
viewModel.download(app)
|
||||||
requireContext(),
|
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
|
||||||
) == PackageManager.PERMISSION_GRANTED
|
|
||||||
) {
|
|
||||||
updateService?.updateApp(app)
|
|
||||||
} else {
|
|
||||||
startForPermissions.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updateService?.updateApp(app)
|
if (ContextCompat.checkSelfPermission(
|
||||||
|
requireContext(),
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
viewModel.download(app)
|
||||||
|
} else {
|
||||||
|
startForPermissions.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
viewModel.download(app)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelSingle(app: App) {
|
private fun cancelSingle(app: App) {
|
||||||
runInService {
|
viewModel.cancelDownload(app)
|
||||||
updateService?.fetch?.cancelGroup(app.getGroupId(requireContext()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelAll() {
|
private fun cancelAll() {
|
||||||
runInService {
|
viewModel.cancelAll()
|
||||||
viewModel.updateAllEnqueued = false
|
|
||||||
updateFileMap.values.forEach {
|
|
||||||
updateService?.fetch?.cancelGroup(
|
|
||||||
it.app.getGroupId(
|
|
||||||
requireContext()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getUpdateServiceInstance() {
|
|
||||||
if (updateService == null && !attachToServiceCalled) {
|
|
||||||
attachToServiceCalled = true
|
|
||||||
val intent = Intent(requireContext(), UpdateService::class.java)
|
|
||||||
requireContext().startService(intent)
|
|
||||||
requireContext().bindService(
|
|
||||||
intent,
|
|
||||||
serviceConnection,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,172 +23,74 @@ import android.app.Application
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.content.pm.PackageInfoCompat
|
import androidx.core.content.pm.PackageInfoCompat
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.aurora.gplayapi.data.models.App
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.store.State
|
|
||||||
import com.aurora.store.data.RequestState
|
|
||||||
import com.aurora.store.data.downloader.RequestGroupIdBuilder
|
|
||||||
import com.aurora.store.data.downloader.getGroupId
|
|
||||||
import com.aurora.store.data.event.BusEvent
|
|
||||||
import com.aurora.store.data.event.InstallerEvent
|
|
||||||
import com.aurora.store.data.installer.AppInstaller
|
import com.aurora.store.data.installer.AppInstaller
|
||||||
import com.aurora.store.data.model.UpdateFile
|
import com.aurora.store.util.DownloadWorkerUtil
|
||||||
import com.tonyodev.fetch2.Download
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import com.tonyodev.fetch2.FetchGroup
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.greenrobot.eventbus.EventBus
|
|
||||||
import org.greenrobot.eventbus.Subscribe
|
|
||||||
import org.greenrobot.eventbus.ThreadMode
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class UpdatesViewModel(application: Application) : BaseAppsViewModel(application) {
|
@HiltViewModel
|
||||||
|
class UpdatesViewModel @Inject constructor(
|
||||||
|
application: Application,
|
||||||
|
private val downloadWorkerUtil: DownloadWorkerUtil
|
||||||
|
) : BaseAppsViewModel(application) {
|
||||||
|
|
||||||
private val TAG = UpdatesViewModel::class.java.simpleName
|
private val TAG = UpdatesViewModel::class.java.simpleName
|
||||||
|
|
||||||
var updateFileMap: MutableMap<Int, UpdateFile> = mutableMapOf()
|
|
||||||
var liveUpdateData: MutableLiveData<MutableMap<Int, UpdateFile>> = MutableLiveData()
|
|
||||||
var updateAllEnqueued: Boolean = false
|
var updateAllEnqueued: Boolean = false
|
||||||
|
|
||||||
init {
|
private val _updates = MutableSharedFlow<List<App>?>()
|
||||||
EventBus.getDefault().register(this)
|
val updates = _updates.asSharedFlow()
|
||||||
|
|
||||||
requestState = RequestState.Init
|
val downloadsList get() = downloadWorkerUtil.downloadsList
|
||||||
observe()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun observe() {
|
override fun observe() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val marketApps = getFilteredApps()
|
getFilteredApps().filter {
|
||||||
checkUpdate(marketApps)
|
val packageInfo = packageInfoMap[it.packageName]
|
||||||
} catch (e: Exception) {
|
if (packageInfo != null) {
|
||||||
requestState = RequestState.Pending
|
it.versionCode.toLong() > PackageInfoCompat.getLongVersionCode(packageInfo)
|
||||||
}
|
} else {
|
||||||
}
|
false
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkUpdate(subAppList: List<App>) {
|
|
||||||
subAppList.filter {
|
|
||||||
val packageInfo = packageInfoMap[it.packageName]
|
|
||||||
if (packageInfo != null) {
|
|
||||||
it.versionCode.toLong() > PackageInfoCompat.getLongVersionCode(packageInfo)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}.sortedBy { it.displayName.lowercase(Locale.getDefault()) }.also { apps ->
|
|
||||||
updateFileMap.clear()
|
|
||||||
|
|
||||||
apps.forEach {
|
|
||||||
updateFileMap[it.getGroupId(getApplication<Application>().applicationContext)] = UpdateFile(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
liveUpdateData.postValue(updateFileMap)
|
|
||||||
requestState = RequestState.Complete
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Subscribe(threadMode = ThreadMode.BACKGROUND)
|
|
||||||
fun onBusEvent(event: BusEvent) {
|
|
||||||
when (event) {
|
|
||||||
is BusEvent.InstallEvent -> {
|
|
||||||
updateListAndPost(event.packageName)
|
|
||||||
}
|
|
||||||
is BusEvent.UninstallEvent -> {
|
|
||||||
updateListAndPost(event.packageName)
|
|
||||||
}
|
|
||||||
is BusEvent.Blacklisted -> {
|
|
||||||
updateListAndPost(event.packageName)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Subscribe(threadMode = ThreadMode.BACKGROUND)
|
|
||||||
fun onInstallerEvent(event: InstallerEvent) {
|
|
||||||
when (event) {
|
|
||||||
is InstallerEvent.Success -> {
|
|
||||||
|
|
||||||
}
|
|
||||||
is InstallerEvent.Cancelled -> {
|
|
||||||
|
|
||||||
}
|
|
||||||
is InstallerEvent.Failed -> {
|
|
||||||
val packageName = event.packageName
|
|
||||||
packageName?.let {
|
|
||||||
val groupIDsOfPackageName = RequestGroupIdBuilder.getGroupIDsForApp(getApplication<Application>().applicationContext, packageName.hashCode())
|
|
||||||
groupIDsOfPackageName.forEach {
|
|
||||||
updateDownload(it, null, true)
|
|
||||||
}
|
}
|
||||||
|
}.sortedBy { it.displayName.lowercase(Locale.getDefault()) }.also { apps ->
|
||||||
|
_updates.emit(apps)
|
||||||
}
|
}
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Log.d(TAG, "Failed to get updates", exception)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateState(id: Int, state: State) {
|
|
||||||
updateFileMap[id]?.state = state
|
|
||||||
liveUpdateData.postValue(updateFileMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateDownload(
|
|
||||||
id: Int,
|
|
||||||
group: FetchGroup?,
|
|
||||||
isCancelled: Boolean = false,
|
|
||||||
isComplete: Boolean = false
|
|
||||||
) {
|
|
||||||
when {
|
|
||||||
isCancelled -> {
|
|
||||||
updateFileMap[id]?.state = State.IDLE
|
|
||||||
updateFileMap[id]?.group = null
|
|
||||||
}
|
|
||||||
isComplete -> {
|
|
||||||
updateFileMap[id]?.state = State.COMPLETE
|
|
||||||
updateFileMap[id]?.group = group
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
updateFileMap[id]?.state = State.PROGRESS
|
|
||||||
updateFileMap[id]?.group = group
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
liveUpdateData.postValue(updateFileMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun install(context: Context, packageName: String, files: List<Download>) {
|
fun install(context: Context, packageName: String, files: List<File>) {
|
||||||
if (files.all { File(it.file).exists() }) {
|
try {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
AppInstaller.getInstance(context).getPreferredInstaller()
|
||||||
AppInstaller.getInstance(context).getPreferredInstaller().install(
|
.install(packageName, files)
|
||||||
packageName,
|
|
||||||
files.filter { it.file.endsWith(".apk") }.map { it.file }.toList()
|
|
||||||
)
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.e(TAG, "Failed to install $packageName", exception)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} catch (exception: Exception) {
|
||||||
Log.e(TAG, "Given files doesn't exists!")
|
Log.e(TAG, "Failed to install app", exception)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateListAndPost(packageName: String) {
|
fun download(app: App) {
|
||||||
val groupIDsOfPackageName = RequestGroupIdBuilder.getGroupIDsForApp(getApplication<Application>().applicationContext, packageName.hashCode())
|
viewModelScope.launch { downloadWorkerUtil.enqueueApp(app) }
|
||||||
groupIDsOfPackageName.forEach {
|
|
||||||
//Remove from map
|
|
||||||
updateFileMap.remove(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
//Post new update list
|
|
||||||
liveUpdateData.postValue(updateFileMap)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
fun cancelDownload(app: App) {
|
||||||
EventBus.getDefault().unregister(this)
|
viewModelScope.launch { downloadWorkerUtil.cancelDownload(app.packageName) }
|
||||||
super.onCleared()
|
}
|
||||||
|
|
||||||
|
fun cancelAll() {
|
||||||
|
viewModelScope.launch { downloadWorkerUtil.cancelAll() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import com.aurora.store.data.providers.AuthProvider
|
|||||||
import com.aurora.store.util.DownloadWorkerUtil
|
import com.aurora.store.util.DownloadWorkerUtil
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import java.io.File
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
@@ -146,7 +147,7 @@ class AppDetailsViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun install(context: Context, packageName: String, files: List<Any>) {
|
fun install(context: Context, packageName: String, files: List<File>) {
|
||||||
try {
|
try {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
AppInstaller.getInstance(context).getPreferredInstaller()
|
AppInstaller.getInstance(context).getPreferredInstaller()
|
||||||
|
|||||||
Reference in New Issue
Block a user