Improve app updates page [1/2]

This commit is contained in:
Rahul Kumar Patel
2021-03-08 00:17:45 +05:30
parent 23ed1bd1a1
commit 527989a393
15 changed files with 526 additions and 202 deletions

View File

@@ -23,3 +23,11 @@ enum class AccountType {
ANONYMOUS, ANONYMOUS,
GOOGLE GOOGLE
} }
enum class State {
IDLE,
QUEUED,
PROGRESS,
COMPLETE,
CANCELED
}

View File

@@ -0,0 +1,49 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.data.model
import com.aurora.gplayapi.data.models.App
import com.aurora.store.State
import com.tonyodev.fetch2.FetchGroup
data class UpdateFile(val app: App) {
var group: FetchGroup? = null
var state: State = State.IDLE
override fun hashCode(): Int {
return app.id
}
override fun equals(other: Any?): Boolean {
return when (other) {
is UpdateFile -> other.app.id == app.id
&& other.state == state
&& isGroupSame(group, other.group)
else -> false
}
}
private fun isGroupSame(oldGroup: FetchGroup?, newGroup: FetchGroup?): Boolean {
if (oldGroup != null && newGroup == null)
return true
return oldGroup?.groupDownloadProgress != newGroup?.groupDownloadProgress
}
}

View File

@@ -30,6 +30,7 @@ import com.aurora.extensions.getString
import com.aurora.extensions.isLAndAbove import com.aurora.extensions.isLAndAbove
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.databinding.ViewActionButtonBinding import com.aurora.store.databinding.ViewActionButtonBinding
import nl.komponents.kovenant.task import nl.komponents.kovenant.task
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -81,6 +82,20 @@ class ActionButton : RelativeLayout {
R.drawable.ic_check R.drawable.ic_check
) )
val stateBackground =
when (typedArray.getString(R.styleable.ActionButton_btnActionBackground)) {
"0" -> R.drawable.bg_state_outline
"1" -> R.drawable.bg_state_outline_rounded
"2" -> R.drawable.bg_state_flat
"3" -> R.drawable.bg_state_flat_rounded
"4" -> null
else -> null
}
stateBackground?.let {
B.root.background = ContextCompat.getDrawable(context, it)
}
val stateColor = ContextCompat.getColor(context, btnTxtColor) val stateColor = ContextCompat.getColor(context, btnTxtColor)
B.btn.text = btnTxt B.btn.text = btnTxt
@@ -108,6 +123,7 @@ class ActionButton : RelativeLayout {
State.IDLE -> 0 State.IDLE -> 0
State.PROGRESS -> 1 State.PROGRESS -> 1
State.COMPLETE -> 2 State.COMPLETE -> 2
else -> 0
} }
if (B.viewFlipper.displayedChild != displayChild) { if (B.viewFlipper.displayedChild != displayChild) {
@@ -127,13 +143,7 @@ class ActionButton : RelativeLayout {
} }
} }
fun addOnClickListener(onClickListener: OnClickListener) { fun addOnClickListener(onClickListener: OnClickListener?) {
B.btn.setOnClickListener(onClickListener) B.btn.setOnClickListener(onClickListener)
} }
enum class State {
IDLE,
PROGRESS,
COMPLETE,
}
} }

View File

@@ -0,0 +1,99 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.custom.layouts.button
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.widget.RelativeLayout
import androidx.annotation.RequiresApi
import com.aurora.extensions.getString
import com.aurora.extensions.runOnUiThread
import com.aurora.store.R
import com.aurora.store.State
import com.aurora.store.databinding.ViewUpdateButtonBinding
class UpdateButton : RelativeLayout {
private lateinit var B: ViewUpdateButtonBinding
constructor(context: Context) : super(context) {
init(context, null)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
init(context, attrs)
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int,
defStyleRes: Int
) : super(context, attrs, defStyleAttr, defStyleRes) {
init(context, attrs)
}
private fun init(context: Context, attrs: AttributeSet?) {
val view = inflate(context, R.layout.view_update_button, this)
B = ViewUpdateButtonBinding.bind(view)
}
fun setText(text: String) {
B.viewFlipper.displayedChild = 0
B.btnPositive.text = text
}
fun setText(text: Int) {
B.viewFlipper.displayedChild = 0
B.btnPositive.text = getString(text)
}
fun updateState(state: State) {
val displayChild = when (state) {
State.IDLE, State.COMPLETE, State.CANCELED -> 0
State.QUEUED -> 1
State.PROGRESS -> 2
}
if (B.viewFlipper.displayedChild != displayChild) {
runOnUiThread {
B.viewFlipper.displayedChild = displayChild
}
}
}
fun addPositiveOnClickListener(onClickListener: OnClickListener?) {
B.btnPositive.setOnClickListener(onClickListener)
}
fun addNegativeOnClickListener(onClickListener: OnClickListener?) {
B.btnNegative.setOnClickListener(onClickListener)
}
}

View File

@@ -21,17 +21,19 @@ package com.aurora.store.view.epoxy.views.app
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.CompoundButton
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import com.airbnb.epoxy.CallbackProp import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView import com.airbnb.epoxy.ModelView
import com.airbnb.epoxy.OnViewRecycled import com.airbnb.epoxy.OnViewRecycled
import com.aurora.extensions.clear import com.aurora.extensions.hide
import com.aurora.extensions.load import com.aurora.extensions.load
import com.aurora.gplayapi.data.models.App import com.aurora.extensions.px
import com.aurora.extensions.show
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.State
import com.aurora.store.data.model.UpdateFile
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.view.epoxy.views.BaseView import com.aurora.store.view.epoxy.views.BaseView
@@ -45,7 +47,6 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withC
class AppUpdateView : RelativeLayout { class AppUpdateView : RelativeLayout {
private lateinit var B: ViewAppUpdateBinding private lateinit var B: ViewAppUpdateBinding
private var expanded: Boolean = false
constructor(context: Context?) : super(context) { constructor(context: Context?) : super(context) {
init(context, null) init(context, null)
@@ -69,43 +70,67 @@ class AppUpdateView : RelativeLayout {
} }
@ModelProp @ModelProp
fun app(app: App) { fun updateFile(updateFile: UpdateFile?) {
B.txtLine1.text = app.displayName if (updateFile != null) {
B.imgIcon.load(app.iconArtwork.url, withCrossFade()) {
placeholder(R.drawable.bg_placeholder)
transform(RoundedCorners(25))
}
B.txtLine2.text = app.developerName /*Inflate App details*/
B.txtLine3.text = ("${CommonUtil.addSiPrefix(app.size)}${app.updatedOn}") with(updateFile.app) {
B.txtLine4.text = ("v${app.versionName}.${app.versionCode}") B.txtLine1.text = displayName
B.txtChangelog.text = if (app.changes.isNotEmpty()) B.imgIcon.load(iconArtwork.url, withCrossFade()) {
HtmlCompat.fromHtml( placeholder(R.drawable.bg_placeholder)
app.changes, transform(RoundedCorners(8.px.toInt()))
HtmlCompat.FROM_HTML_OPTION_USE_CSS_COLORS }
)
else
context.getString(R.string.details_changelog_unavailable)
B.headerIndicator.setOnClickListener { B.txtLine2.text = developerName
B.expansionLayout.let { B.txtLine3.text = ("${CommonUtil.addSiPrefix(size)}$updatedOn")
if (it.isExpanded) { B.txtLine4.text = ("v${versionName}.${versionCode}")
it.collapse(true) B.txtChangelog.text = if (changes.isNotEmpty())
} else { HtmlCompat.fromHtml(
it.expand(true) changes,
HtmlCompat.FROM_HTML_OPTION_USE_CSS_COLORS
)
else
context.getString(R.string.details_changelog_unavailable)
B.headerIndicator.setOnClickListener {
B.expansionLayout.let {
if (it.isExpanded) {
it.collapse(true)
} else {
it.expand(true)
}
}
}
}
/*Inflate Download details*/
updateFile.group?.let {
if (updateFile.state == State.PROGRESS) {
val progress = it.groupDownloadProgress
if (progress > 0) {
if (progress == 100) {
B.txtProgressPercent.hide()
} else {
B.txtProgressPercent.show()
B.txtProgressPercent.text = ("${progress}%")
}
}
} else if (updateFile.state == State.IDLE || updateFile.state == State.CANCELED) {
B.btnAction.updateState(State.IDLE)
B.txtProgressPercent.text = ("0%")
B.txtProgressPercent.hide()
} else if (updateFile.state == State.QUEUED) {
B.btnAction.updateState(State.QUEUED)
} }
} }
} }
} }
@ModelProp @ModelProp
fun markChecked(isChecked: Boolean) { fun state(state: State?) {
B.checkbox.isChecked = isChecked state?.let {
} B.btnAction.updateState(it)
}
@CallbackProp
fun checked(onCheckedChangeListener: CompoundButton.OnCheckedChangeListener?) {
B.checkbox.setOnCheckedChangeListener(onCheckedChangeListener)
} }
@CallbackProp @CallbackProp
@@ -113,6 +138,16 @@ class AppUpdateView : RelativeLayout {
B.layoutContent.setOnClickListener(onClickListener) B.layoutContent.setOnClickListener(onClickListener)
} }
@CallbackProp
fun positiveAction(onClickListener: OnClickListener?) {
B.btnAction.addPositiveOnClickListener(onClickListener)
}
@CallbackProp
fun negativeAction(onClickListener: OnClickListener?) {
B.btnAction.addNegativeOnClickListener(onClickListener)
}
@CallbackProp @CallbackProp
fun longClick(onClickListener: OnLongClickListener?) { fun longClick(onClickListener: OnLongClickListener?) {
B.layoutContent.setOnLongClickListener(onClickListener) B.layoutContent.setOnLongClickListener(onClickListener)
@@ -120,7 +155,6 @@ class AppUpdateView : RelativeLayout {
@OnViewRecycled @OnViewRecycled
fun clear() { fun clear() {
B.imgIcon.clear()
B.headerIndicator.removeCallbacks { } B.headerIndicator.removeCallbacks { }
} }
} }

View File

@@ -35,8 +35,6 @@ import java.lang.reflect.Modifier
open class BaseFragment : Fragment() { open class BaseFragment : Fragment() {
protected lateinit var app: App
var gson: Gson = GsonBuilder().excludeFieldsWithModifiers( var gson: Gson = GsonBuilder().excludeFieldsWithModifiers(
Modifier.TRANSIENT Modifier.TRANSIENT
).create() ).create()

View File

@@ -38,6 +38,7 @@ import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.gplayapi.helpers.PurchaseHelper import com.aurora.gplayapi.helpers.PurchaseHelper
import com.aurora.store.MainActivity import com.aurora.store.MainActivity
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.State
import com.aurora.store.data.downloader.DownloadManager import com.aurora.store.data.downloader.DownloadManager
import com.aurora.store.data.downloader.RequestBuilder import com.aurora.store.data.downloader.RequestBuilder
import com.aurora.store.data.event.BusEvent import com.aurora.store.data.event.BusEvent
@@ -47,7 +48,6 @@ import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AuthProvider import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.databinding.ActivityDetailsBinding import com.aurora.store.databinding.ActivityDetailsBinding
import com.aurora.store.util.* import com.aurora.store.util.*
import com.aurora.store.view.custom.layouts.button.ActionButton
import com.aurora.store.view.ui.downloads.DownloadActivity import com.aurora.store.view.ui.downloads.DownloadActivity
import com.aurora.store.view.ui.sheets.InstallErrorDialogSheet import com.aurora.store.view.ui.sheets.InstallErrorDialogSheet
import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.load.resource.bitmap.RoundedCorners
@@ -125,7 +125,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
event.extra event.extra
).show(supportFragmentManager, "SED") ).show(supportFragmentManager, "SED")
attachActions() attachActions()
updateActionState(ActionButton.State.IDLE) updateActionState(State.IDLE)
} }
} }
else -> { else -> {
@@ -234,7 +234,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
checkAndSetupInstall() checkAndSetupInstall()
} }
private fun updateActionState(state: ActionButton.State) { private fun updateActionState(state: State) {
B.layoutDetailsInstall.btnDownload.updateState(state) B.layoutDetailsInstall.btnDownload.updateState(state)
} }
@@ -251,7 +251,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
@Synchronized @Synchronized
private fun install(files: List<Download>) { private fun install(files: List<Download>) {
updateActionState(ActionButton.State.IDLE) updateActionState(State.IDLE)
task { task {
AppInstaller(this) AppInstaller(this)
@@ -398,7 +398,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
} }
private fun purchase() { private fun purchase() {
updateActionState(ActionButton.State.PROGRESS) updateActionState(State.PROGRESS)
task { task {
val authData = AuthProvider val authData = AuthProvider
@@ -424,10 +424,10 @@ class AppDetailsActivity : BaseDetailsActivity() {
enqueue(files) enqueue(files)
} else { } else {
Log.e("Failed to download : ${app.displayName}") Log.e("Failed to download : ${app.displayName}")
updateActionState(ActionButton.State.IDLE) updateActionState(State.IDLE)
} }
} failUi { } failUi {
updateActionState(ActionButton.State.IDLE) updateActionState(State.IDLE)
var reason = "Unknown" var reason = "Unknown"
when (it) { when (it) {
@@ -477,7 +477,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
Log.i("Downloading Apks : %s", app.displayName) Log.i("Downloading Apks : %s", app.displayName)
} }
} else { } else {
updateActionState(ActionButton.State.IDLE) updateActionState(State.IDLE)
expandBottomSheet(getString(R.string.purchase_session_expired)) expandBottomSheet(getString(R.string.purchase_session_expired))
} }
} }
@@ -521,7 +521,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
with(B.layoutDetailsInstall) { with(B.layoutDetailsInstall) {
txtPurchaseError.text = message txtPurchaseError.text = message
btnDownload.updateState(ActionButton.State.IDLE) btnDownload.updateState(State.IDLE)
if (app.isFree) if (app.isFree)
btnDownload.setText(R.string.action_install) btnDownload.setText(R.string.action_install)
else else

View File

@@ -25,20 +25,21 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.aurora.Constants import com.aurora.Constants
import com.aurora.extensions.toast
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.AuthData
import com.aurora.gplayapi.helpers.PurchaseHelper import com.aurora.gplayapi.helpers.PurchaseHelper
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.State
import com.aurora.store.data.downloader.DownloadManager import com.aurora.store.data.downloader.DownloadManager
import com.aurora.store.data.downloader.RequestBuilder import com.aurora.store.data.downloader.RequestBuilder
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.data.providers.AuthProvider import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.databinding.FragmentUpdatesBinding import com.aurora.store.databinding.FragmentUpdatesBinding
import com.aurora.store.util.Log import com.aurora.store.util.Log
import com.aurora.extensions.flushAndAdd
import com.aurora.extensions.toast
import com.aurora.store.view.epoxy.views.app.AppUpdateViewModel_
import com.aurora.store.view.epoxy.views.UpdateHeaderViewModel_ import com.aurora.store.view.epoxy.views.UpdateHeaderViewModel_
import com.aurora.store.view.epoxy.views.app.AppUpdateViewModel_
import com.aurora.store.view.epoxy.views.app.NoAppViewModel_ 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
@@ -63,7 +64,7 @@ class UpdatesFragment : BaseFragment() {
private lateinit var fetch: Fetch private lateinit var fetch: Fetch
private lateinit var fetchListener: AbstractFetchGroupListener private lateinit var fetchListener: AbstractFetchGroupListener
private val appList: MutableList<App> = mutableListOf() private var updateFileMap: MutableMap<Int, UpdateFile> = mutableMapOf()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@@ -85,14 +86,36 @@ class UpdatesFragment : BaseFragment() {
fetch = DownloadManager.with(requireContext()).fetch fetch = DownloadManager.with(requireContext()).fetch
fetchListener = object : AbstractFetchGroupListener() { fetchListener = object : AbstractFetchGroupListener() {
override fun onAdded(groupId: Int, download: Download, fetchGroup: FetchGroup) {
VM.updateDownload(groupId, fetchGroup)
}
override fun onProgress(
groupId: Int,
download: Download,
etaInMilliSeconds: Long,
downloadedBytesPerSecond: Long,
fetchGroup: FetchGroup
) {
VM.updateDownload(groupId, fetchGroup)
}
override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) { override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
super.onCompleted(groupId, download, fetchGroup)
if (fetchGroup.groupDownloadProgress == 100) { if (fetchGroup.groupDownloadProgress == 100) {
VM.updateDownload(groupId, fetchGroup)
install(download.tag!!, fetchGroup.downloads) install(download.tag!!, fetchGroup.downloads)
} }
} }
}
override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) {
VM.updateDownload(groupId, fetchGroup,true)
}
override fun onDeleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
VM.updateDownload(groupId, fetchGroup,true)
}
}
return B.root return B.root
} }
@@ -113,9 +136,10 @@ class UpdatesFragment : BaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
VM.liveData.observe(viewLifecycleOwner, {
appList.flushAndAdd(it) VM.liveUpdateData.observe(viewLifecycleOwner, {
updateController(it) updateFileMap = it
updateController(updateFileMap)
B.swipeRefreshLayout.isRefreshing = false B.swipeRefreshLayout.isRefreshing = false
}) })
@@ -126,10 +150,10 @@ class UpdatesFragment : BaseFragment() {
updateController(null) updateController(null)
} }
private fun updateController(appList: List<App>?) { private fun updateController(updateFileMap: MutableMap<Int, UpdateFile>?) {
B.recycler.withModels { B.recycler.withModels {
setFilterDuplicates(true) setFilterDuplicates(true)
if (appList == null) { if (updateFileMap == null) {
for (i in 1..6) { for (i in 1..6) {
add( add(
AppListViewShimmerModel_() AppListViewShimmerModel_()
@@ -137,7 +161,7 @@ class UpdatesFragment : BaseFragment() {
) )
} }
} else { } else {
if (appList.isEmpty()) { if (updateFileMap.isEmpty()) {
add( add(
NoAppViewModel_() NoAppViewModel_()
.id("no_update") .id("no_update")
@@ -146,44 +170,26 @@ class UpdatesFragment : BaseFragment() {
) )
} else { } else {
add( add(
if (VM.selectedUpdates.isEmpty()) { UpdateHeaderViewModel_()
UpdateHeaderViewModel_() .id("header_all")
.id("header_all") .title("${updateFileMap.size} updates available")
.title("${appList.size} updates available") .action(getString(R.string.action_update_all))
.action(getString(R.string.action_update_all)) .click { _ -> updateAll() }
.click { v ->
updateAction(false)
}
} else {
UpdateHeaderViewModel_()
.id("header_selected")
.title("${VM.selectedUpdates.size} updates selected")
.action(getString(R.string.action_update))
.click { v ->
updateAction(true)
}
}
) )
appList.forEach { app -> updateFileMap.values.forEach { updateFile ->
add( add(
AppUpdateViewModel_() AppUpdateViewModel_()
.id(app.id) .id(updateFile.hashCode())
.app(app) .updateFile(updateFile)
.click { _ -> openDetailsActivity(app) } .click { _ -> openDetailsActivity(updateFile.app) }
.longClick { _ -> .longClick { _ ->
openAppMenuSheet(app) openAppMenuSheet(updateFile.app)
false false
} }
.markChecked(VM.selectedUpdates.contains(app.packageName)) .positiveAction { _ -> updateSingle(updateFile.app) }
.checked { _, isChecked -> .negativeAction { _ -> cancelSingle(updateFile.app) }
if (isChecked) .state(updateFile.state)
VM.selectedUpdates.add(app.packageName)
else
VM.selectedUpdates.remove(app.packageName)
requestModelBuild()
}
) )
} }
} }
@@ -191,65 +197,38 @@ class UpdatesFragment : BaseFragment() {
} }
} }
private fun updateAction(selectedOnly: Boolean) { private fun updateSingle(app: App) {
if (VM.isUpdating) {
requireContext().toast("Update in progress, let previous batch finish first") VM.updateState(app.id, State.QUEUED)
} else {
if (selectedOnly) { task {
updateSelected() val files = purchaseHelper.purchase(
app.packageName,
app.versionCode,
app.offerType
)
files.map { RequestBuilder.buildRequest(requireContext(), app, it) }
} successUi {
val requests = it.filter { request -> request.url.isNotEmpty() }.toList()
if (requests.isNotEmpty()) {
fetch.enqueue(requests) {
Log.i("Updating ${app.displayName}")
}
} else { } else {
updateAll() requireContext().toast("Failed to update ${app.displayName}")
} }
} failUi {
Log.e("Failed to update ${app.displayName}")
} }
} }
private fun cancelSingle(app: App) {
fetch.cancelGroup(app.id)
}
private fun updateAll() { private fun updateAll() {
appList.forEach { app -> updateFileMap.values.forEach { updateSingle(it.app) }
task {
val files = purchaseHelper.purchase(app.packageName, app.versionCode, app.offerType)
files.map { RequestBuilder.buildRequest(requireContext(), app, it) }
} successUi {
val requests = it.filter { request -> request.url.isNotEmpty() }.toList()
if (requests.isNotEmpty()) {
VM.isUpdating = true
fetch.enqueue(requests) {
Log.i("Updating ${app.displayName}")
}
} else {
requireContext().toast("Failed to update ${app.displayName}")
}
} failUi {
Log.e("Failed to update ${app.displayName}")
}
}
}
private fun updateSelected() {
val selectedPackages = VM.selectedUpdates
appList.forEach { app ->
if (selectedPackages.contains(app.packageName)) {
task {
val files = purchaseHelper.purchase(
app.packageName,
app.versionCode,
app.offerType
)
files.map { RequestBuilder.buildRequest(requireContext(), app, it) }
} successUi {
val requests = it.filter { request -> request.url.isNotEmpty() }.toList()
if (requests.isNotEmpty()) {
VM.isUpdating = true
fetch.enqueue(requests) {
Log.i("Updating ${app.displayName}")
}
} else {
requireContext().toast("Failed to update ${app.displayName}")
}
} failUi {
Log.e("Failed to update ${app.displayName}")
}
}
}
} }
@Synchronized @Synchronized

View File

@@ -20,11 +20,15 @@
package com.aurora.store.viewmodel.all package com.aurora.store.viewmodel.all
import android.app.Application import android.app.Application
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.RequestState
import com.aurora.store.data.event.BusEvent import com.aurora.store.data.event.BusEvent
import com.aurora.extensions.flushAndAdd import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.model.UpdateFile
import com.tonyodev.fetch2.FetchGroup
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
@@ -33,8 +37,8 @@ import org.greenrobot.eventbus.ThreadMode
class UpdatesViewModel(application: Application) : BaseAppsViewModel(application) { class UpdatesViewModel(application: Application) : BaseAppsViewModel(application) {
var selectedUpdates: MutableSet<String> = mutableSetOf() var updateFileMap: MutableMap<Int, UpdateFile> = mutableMapOf()
var isUpdating: Boolean = false var liveUpdateData: MutableLiveData<MutableMap<Int, UpdateFile>> = MutableLiveData()
init { init {
EventBus.getDefault().register(this) EventBus.getDefault().register(this)
@@ -63,14 +67,17 @@ class UpdatesViewModel(application: Application) : BaseAppsViewModel(application
false false
} }
}.also { apps -> }.also { apps ->
appList.flushAndAdd(apps) apps.forEach {
liveData.postValue(appList.sortedBy { it.displayName }) updateFileMap[it.id] = UpdateFile(it)
}
liveUpdateData.postValue(updateFileMap)
requestState = RequestState.Complete requestState = RequestState.Complete
} }
} }
@Subscribe(threadMode = ThreadMode.BACKGROUND) @Subscribe(threadMode = ThreadMode.BACKGROUND)
fun onEvent(event: BusEvent) { fun onBusEvent(event: BusEvent) {
when (event) { when (event) {
is BusEvent.InstallEvent -> { is BusEvent.InstallEvent -> {
updateListAndPost(event.packageName) updateListAndPost(event.packageName)
@@ -87,22 +94,44 @@ class UpdatesViewModel(application: Application) : BaseAppsViewModel(application
} }
} }
@Subscribe(threadMode = ThreadMode.BACKGROUND)
fun onInstallerEvent(event: InstallerEvent) {
when (event) {
is InstallerEvent.Success -> {
}
is InstallerEvent.Failed -> {
val packageName = event.packageName
packageName?.let {
updateDownload(packageName.hashCode(), null, true)
}
}
}
}
fun updateState(id: Int, state: State) {
updateFileMap[id]?.state = state
liveUpdateData.postValue(updateFileMap)
}
fun updateDownload(id: Int, group: FetchGroup?, isCancelled: Boolean = false) {
if (isCancelled) {
updateFileMap[id]?.state = State.IDLE
updateFileMap[id]?.group = null
} else {
updateFileMap[id]?.state = State.PROGRESS
updateFileMap[id]?.group = group
}
liveUpdateData.postValue(updateFileMap)
}
private fun updateListAndPost(packageName: String) { private fun updateListAndPost(packageName: String) {
//Remove from selected updates //Remove from map
selectedUpdates.remove(packageName) updateFileMap.remove(packageName.hashCode())
if (selectedUpdates.isEmpty())
isUpdating = false
//Remove from current update list
val updatedList = appList.filter {
it.packageName != packageName
}.toList()
appList.flushAndAdd(updatedList)
//Post new update list //Post new update list
liveData.postValue(appList.sortedBy { it.displayName }) liveUpdateData.postValue(updateFileMap)
} }
override fun onCleared() { override fun onCleared() {

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="8dp" />
<solid android:color="?colorAccent" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="28dp" />
<solid android:color="?colorAccent" />
</shape>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="8dp"/>
<stroke
android:width="1dp"
android:color="@color/colorPrimaryAlt" />
</shape>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="28dp"/>
<stroke
android:width="1dp"
android:color="@color/colorPrimaryAlt" />
</shape>

View File

@@ -41,19 +41,40 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/img_icon" <RelativeLayout
android:id="@+id/head_flipper"
android:layout_width="@dimen/icon_size_medium" android:layout_width="@dimen/icon_size_medium"
android:layout_height="@dimen/icon_size_medium" /> android:layout_height="@dimen/icon_size_medium">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/img_icon"
android:layout_width="@dimen/icon_size_medium"
android:layout_height="@dimen/icon_size_medium" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_progress_percent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:visibility="gone"
android:background="@drawable/bg_rounded"
android:gravity="center"
android:paddingStart="@dimen/padding_normal"
android:paddingEnd="@dimen/padding_normal"
android:text="0%"
android:textAlignment="center"
android:textSize="12sp"
app:fontFamily="@font/gilroy_extra_bold" />
</RelativeLayout>
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_line1" android:id="@+id/txt_line1"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_normal" android:layout_marginStart="@dimen/margin_normal"
android:layout_marginEnd="@dimen/margin_normal" android:layout_toStartOf="@+id/headerIndicator"
android:layout_toStartOf="@+id/layout_action" android:layout_toEndOf="@id/head_flipper"
android:layout_toEndOf="@id/img_icon"
android:ellipsize="end" android:ellipsize="end"
android:singleLine="true" android:singleLine="true"
android:textAppearance="@style/TextAppearance.Aurora.Line1" /> android:textAppearance="@style/TextAppearance.Aurora.Line1" />
@@ -91,41 +112,32 @@
android:singleLine="true" android:singleLine="true"
android:textAppearance="@style/TextAppearance.Aurora.Line3" /> android:textAppearance="@style/TextAppearance.Aurora.Line3" />
<LinearLayout <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/layout_action" android:id="@+id/headerIndicator"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignBottom="@id/layout_action"
android:layout_toStartOf="@id/layout_action"
android:adjustViewBounds="true"
android:clickable="true"
android:focusable="true"
android:padding="@dimen/margin_small"
app:srcCompat="@drawable/ic_arrow_right"
app:tint="?colorControlNormal" />
<LinearLayout
android:id="@+id/layout_action"
android:layout_width="100dp"
android:layout_height="@dimen/height_button"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:gravity="center" android:gravity="center"
android:orientation="horizontal" android:orientation="horizontal">
android:weightSum="2">
<androidx.appcompat.widget.AppCompatImageView <com.aurora.store.view.custom.layouts.button.UpdateButton
android:id="@+id/headerIndicator" android:id="@+id/btn_action"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content" />
android:layout_weight="1"
android:adjustViewBounds="true"
android:clickable="true"
android:focusable="true"
android:padding="@dimen/margin_small"
app:srcCompat="@drawable/ic_arrow_right"
app:tint="?colorControlNormal" />
<View
android:layout_width="1dp"
android:layout_height="@dimen/icon_size_small"
android:layout_marginStart="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_small"
android:background="@drawable/bg_placeholder"
android:backgroundTint="?android:colorControlNormal" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/checkbox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout> </LinearLayout>
</RelativeLayout> </RelativeLayout>
</com.github.florent37.expansionpanel.ExpansionHeader> </com.github.florent37.expansionpanel.ExpansionHeader>

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ Aurora Store is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 2 of the License, or
~ (at your option) any later version.
~
~ Aurora Store is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
~
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ViewFlipper
android:id="@+id/view_flipper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inAnimation="@anim/fade_in"
android:outAnimation="@anim/fade_out"
tools:ignore="UselessParent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnPositive"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/action_update"
android:textAppearance="@style/TextAppearance.Aurora.Line1" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnQueued"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
android:text="@string/download_queued"
android:textAppearance="@style/TextAppearance.Aurora.Line1" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnNegative"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/action_cancel"
android:textAppearance="@style/TextAppearance.Aurora.Line1"
android:textColor="@color/colorRed" />
</RelativeLayout>
</ViewFlipper>
</RelativeLayout>