Merge branch 'dev' into 'master'

Improve AppUpdate pregressbar UI

See merge request AuroraOSS/AuroraStore!346
This commit is contained in:
Rahul Patel
2024-07-12 23:36:11 +00:00
48 changed files with 373 additions and 1311 deletions

View File

@@ -25,7 +25,7 @@ import androidx.core.content.ContextCompat
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import com.aurora.extensions.isPAndAbove
import com.aurora.store.data.event.FlowEvent
import com.aurora.store.data.event.EventFlow
import com.aurora.store.data.receiver.PackageManagerReceiver
import com.aurora.store.util.CommonUtil
import com.aurora.store.util.DownloadWorkerUtil
@@ -58,7 +58,7 @@ class AuroraApp : Application(), Configuration.Provider {
private set
val enqueuedInstalls: MutableSet<String> = mutableSetOf()
val flowEvent = FlowEvent()
val events = EventFlow()
}
override fun onCreate() {

View File

@@ -21,48 +21,27 @@ package com.aurora.store.data.event
abstract class Event
sealed class BusEvent: Event() {
data class InstallEvent(
var packageName: String,
var extra: String? = ""
) : BusEvent()
sealed class BusEvent : Event() {
lateinit var extra: String
lateinit var error: String
data class UninstallEvent(
var packageName: String,
var extra: String? = ""
) : BusEvent()
data class Blacklisted(
var packageName: String,
var error: String? = ""
) : BusEvent()
data class GoogleAAS(
var success: Boolean,
var email: String = String(),
var aasToken: String = String()
) : BusEvent()
data class ManualDownload(
var packageName: String,
var versionCode: Int
) : BusEvent()
data class Blacklisted(val packageName: String) : BusEvent()
data class ManualDownload(val packageName: String, val versionCode: Int) : BusEvent()
}
sealed class InstallerEvent: Event() {
data class Success(
var packageName: String? = "",
var extra: String? = ""
) : InstallerEvent()
data class Cancelled(
var packageName: String? = "",
var extra: String? = ""
) : InstallerEvent()
data class Failed(
var packageName: String? = "",
var error: String? = "",
var extra: String? = ""
) : InstallerEvent()
sealed class AuthEvent : Event() {
data class GoogleLogin(val success: Boolean, val email: String, val token: String) : AuthEvent()
}
sealed class InstallerEvent : Event() {
lateinit var extra: String
lateinit var error: String
var progress: Int = -1
data class Installed(val packageName: String) : InstallerEvent()
data class Uninstalled(val packageName: String) : InstallerEvent()
data class Installing(val packageName: String) : InstallerEvent()
data class Cancelled(val packageName: String) : InstallerEvent()
data class Failed(val packageName: String) : InstallerEvent()
}

View File

@@ -6,9 +6,9 @@ import kotlinx.coroutines.flow.asSharedFlow
import javax.inject.Singleton
@Singleton
class FlowEvent {
class EventFlow {
private val TAG = FlowEvent::class.java.simpleName
private val TAG = EventFlow::class.java.simpleName
private val _busEvent = MutableSharedFlow<BusEvent>(extraBufferCapacity = 1)
val busEvent = _busEvent.asSharedFlow()
@@ -16,10 +16,14 @@ class FlowEvent {
private val _installerEvent = MutableSharedFlow<InstallerEvent>(extraBufferCapacity = 1)
val installerEvent = _installerEvent.asSharedFlow()
fun emitEvent(event: Event) {
private val _authEvent = MutableSharedFlow<AuthEvent>(extraBufferCapacity = 1)
val authEvent = _authEvent.asSharedFlow()
fun send(event: Event) {
when (event) {
is InstallerEvent -> _installerEvent.tryEmit(event)
is BusEvent -> _busEvent.tryEmit(event)
is AuthEvent -> _authEvent.tryEmit(event)
else -> Log.e(TAG, "Got an unhandled event")
}
}

View File

@@ -66,16 +66,19 @@ abstract class InstallerBase(protected var context: Context) : IInstaller {
open fun postError(packageName: String, error: String?, extra: String?) {
Log.e("Service Error :$error")
val event = InstallerEvent.Failed(
packageName,
error,
extra
)
val event = InstallerEvent.Failed(packageName).apply {
this.error = error ?: ""
this.extra = extra ?: ""
}
AuroraApp.flowEvent.emitEvent(event)
AuroraApp.events.send(event)
}
open fun getFiles(packageName: String, versionCode: Int, sharedLibPackageName: String = ""): List<File> {
open fun getFiles(
packageName: String,
versionCode: Int,
sharedLibPackageName: String = ""
): List<File> {
val downloadDir = if (sharedLibPackageName.isNotBlank()) {
PathUtil.getLibDownloadDir(context, packageName, versionCode, sharedLibPackageName)
} else {

View File

@@ -104,12 +104,11 @@ class RootInstaller @Inject constructor(
if (packageName == download?.packageName) onInstallationSuccess()
} else {
removeFromInstallQueue(packageName)
val event = InstallerEvent.Failed(
packageName,
context.getString(R.string.installer_status_failure),
parseError(shellResult)
)
AuroraApp.flowEvent.emitEvent(event)
val event = InstallerEvent.Failed(packageName).apply {
this.extra = context.getString(R.string.installer_status_failure)
this.error = parseError(shellResult)
}
AuroraApp.events.send(event)
}
} else {
removeFromInstallQueue(packageName)

View File

@@ -35,7 +35,6 @@ import com.aurora.services.IPrivilegedService
import com.aurora.store.AuroraApp
import com.aurora.store.BuildConfig
import com.aurora.store.R
import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.model.InstallerInfo
import com.aurora.store.data.room.download.Download
@@ -91,6 +90,7 @@ class ServiceInstaller @Inject constructor(
fileList.map { it.absolutePath }
)
}
else -> {
postError(
download.packageName,
@@ -148,7 +148,11 @@ class ServiceInstaller @Inject constructor(
)
} catch (e: RemoteException) {
removeFromInstallQueue(packageName)
postError(packageName, e.localizedMessage, e.stackTraceToString())
postError(
packageName,
e.localizedMessage,
e.stackTraceToString()
)
readyWithAction.set(true)
}
} else {
@@ -166,7 +170,11 @@ class ServiceInstaller @Inject constructor(
)
} catch (e: RemoteException) {
removeFromInstallQueue(packageName)
postError(packageName, e.localizedMessage, e.stackTraceToString())
postError(
packageName,
e.localizedMessage,
e.stackTraceToString()
)
readyWithAction.set(true)
}
}
@@ -210,13 +218,13 @@ class ServiceInstaller @Inject constructor(
try {
when (returnCode) {
PackageInstaller.STATUS_SUCCESS -> {
AuroraApp.flowEvent.emitEvent(
BusEvent.UninstallEvent(
packageName,
context.getString(R.string.installer_status_success)
)
AuroraApp.events.send(
InstallerEvent.Uninstalled(packageName).apply {
this.extra = context.getString(R.string.action_uninstall_success)
}
)
}
else -> {
val error = AppInstaller.getErrorString(
context,
@@ -240,15 +248,15 @@ class ServiceInstaller @Inject constructor(
try {
when (returnCode) {
PackageInstaller.STATUS_SUCCESS -> {
AuroraApp.flowEvent.emitEvent(
InstallerEvent.Success(
packageName,
context.getString(R.string.installer_status_success)
)
AuroraApp.events.send(
InstallerEvent.Installed(packageName).apply {
this.extra = context.getString(R.string.installer_status_success)
}
)
// Installation is not yet finished if this is a shared library
if (packageName == download?.packageName) onInstallationSuccess()
}
else -> {
val error = AppInstaller.getErrorString(
context,

View File

@@ -36,7 +36,9 @@ import com.aurora.extensions.isSAndAbove
import com.aurora.extensions.isTAndAbove
import com.aurora.extensions.isUAndAbove
import com.aurora.extensions.runOnUiThread
import com.aurora.store.AuroraApp
import com.aurora.store.R
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.installer.AppInstaller.Companion.ACTION_INSTALL_STATUS
import com.aurora.store.data.installer.AppInstaller.Companion.EXTRA_DISPLAY_NAME
import com.aurora.store.data.installer.AppInstaller.Companion.EXTRA_PACKAGE_NAME
@@ -48,6 +50,7 @@ import com.aurora.store.data.room.download.Download
import com.aurora.store.util.Log
import com.aurora.store.util.PackageUtil.isSharedLibraryInstalled
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton
@@ -69,27 +72,38 @@ class SessionInstaller @Inject constructor(
override fun onActiveChanged(sessionId: Int, active: Boolean) {}
override fun onProgressChanged(sessionId: Int, progress: Float) {}
override fun onProgressChanged(sessionId: Int, progress: Float) {
val packageName = enqueuedSessions
.find { set -> set.any { it.sessionId == sessionId } }
?.first()
?.packageName
if (packageName != null && progress > 0.0) {
AuroraApp.events.send(
InstallerEvent.Installing(packageName).apply {
this.progress = (progress * 100).toInt()
}
)
}
}
override fun onFinished(sessionId: Int, success: Boolean) {
enqueuedSessions.find { set -> set.any { it.sessionId == sessionId } }?.let { sessionSet ->
sessionSet.remove(sessionSet.first { it.sessionId == sessionId })
enqueuedSessions.find { set -> set.any { it.sessionId == sessionId } }
?.let { sessionSet ->
sessionSet.remove(sessionSet.first { it.sessionId == sessionId })
// If this was a shared lib, proceed installing other libs or actual package
if (sessionSet.isNotEmpty() && success) {
commitInstall(sessionSet.first()); return
} else {
enqueuedSessions.remove(sessionSet)
// If this was a shared lib, proceed installing other libs or actual package
if (sessionSet.isNotEmpty() && success) {
commitInstall(sessionSet.first()); return
} else {
enqueuedSessions.remove(sessionSet)
}
}
}
if (enqueuedSessions.isNotEmpty()) {
enqueuedSessions.firstOrNull()?.let { sessionSet ->
commitInstall(sessionSet.first())
}
} else {
// Nothing else in queue, unregister callback
packageInstaller.unregisterSessionCallback(this)
}
}
}
@@ -113,7 +127,8 @@ class SessionInstaller @Inject constructor(
override fun install(download: Download) {
super.install(download)
val sessionSet = enqueuedSessions.find { set -> set.any { it.packageName == download.packageName } }
val sessionSet =
enqueuedSessions.find { set -> set.any { it.packageName == download.packageName } }
if (sessionSet != null) {
Log.i("${download.packageName} already queued")
commitInstall(sessionSet.first())
@@ -124,7 +139,11 @@ class SessionInstaller @Inject constructor(
download.sharedLibs.forEach {
// Shared library packages cannot be updated
if (!isSharedLibraryInstalled(context, it.packageName, it.versionCode)) {
stageInstall(download.packageName, download.versionCode, it.packageName)?.let { sessionID ->
stageInstall(
download.packageName,
download.versionCode,
it.packageName
)?.let { sessionID ->
sessionInfoSet.add(SessionInfo(sessionID, it.packageName, it.versionCode))
}
}
@@ -147,18 +166,50 @@ class SessionInstaller @Inject constructor(
}
}
private fun stageInstall(packageName: String, versionCode: Int, sharedLibPkgName: String = ""): Int? {
val packageInstaller = context.packageManager.packageInstaller
private fun stageInstall(
packageName: String,
versionCode: Int,
sharedLibPkgName: String = ""
): Int? {
val resolvedPackageName = sharedLibPkgName.ifBlank { packageName }
val sessionParams = SessionParams(SessionParams.MODE_FULL_INSTALL).apply {
setAppPackageName(sharedLibPkgName.ifBlank { packageName })
setInstallLocation(PackageInfo.INSTALL_LOCATION_AUTO)
if (isOAndAbove()) {
setInstallReason(PackageManager.INSTALL_REASON_USER)
val sessionParams = buildSessionParams(resolvedPackageName)
val sessionId = packageInstaller.createSession(sessionParams)
val session = packageInstaller.openSession(sessionId)
return try {
Log.i("Writing splits to session for $packageName")
getFiles(packageName, versionCode, sharedLibPkgName).forEach { file ->
file.inputStream().use { input ->
session.openWrite(
"${resolvedPackageName}_${file.name}",
0,
file.length()
).use { output ->
input.copyTo(output)
session.fsync(output)
}
}
}
sessionId
} catch (exception: IOException) {
session.abandon()
removeFromInstallQueue(packageName)
postError(packageName, exception.localizedMessage, exception.stackTraceToString())
null
}
}
private fun buildSessionParams(packageName: String): SessionParams {
return SessionParams(SessionParams.MODE_FULL_INSTALL).apply {
setAppPackageName(packageName)
setInstallLocation(PackageInfo.INSTALL_LOCATION_AUTO)
if (isNAndAbove()) {
setOriginatingUid(Process.myUid())
}
if (isOAndAbove()) {
setInstallReason(PackageManager.INSTALL_REASON_USER)
}
if (isSAndAbove()) {
setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED)
}
@@ -170,26 +221,6 @@ class SessionInstaller @Inject constructor(
setRequestUpdateOwnership(true)
}
}
val sessionId = packageInstaller.createSession(sessionParams)
val session = packageInstaller.openSession(sessionId)
return try {
Log.i("Writing splits to session for $packageName")
getFiles(packageName, versionCode, sharedLibPkgName).forEach {
it.inputStream().use { input ->
session.openWrite("${sharedLibPkgName.ifBlank { packageName }}_${it.name}", 0, it.length()).use { output ->
input.copyTo(output)
session.fsync(output)
}
}
}
sessionId
} catch (exception: Exception) {
session.abandon()
removeFromInstallQueue(packageName)
postError(packageName, exception.localizedMessage, exception.stackTraceToString())
null
}
}
private fun commitInstall(sessionInfo: SessionInfo) {

View File

@@ -29,5 +29,6 @@ enum class State {
QUEUED,
PROGRESS,
COMPLETE,
CANCELED
CANCELED,
INSTALLING,
}

View File

@@ -0,0 +1,8 @@
package com.aurora.store.data.model
import com.aurora.gplayapi.data.models.App
data class PaginatedAppList(
val appList: MutableList<App> = mutableListOf(),
var hasMore: Boolean
)

View File

@@ -80,7 +80,12 @@ class InstallerStatusReceiver : BroadcastReceiver() {
}
}
private fun notifyUser(context: Context, packageName: String, displayName: String, status: Int) {
private fun notifyUser(
context: Context,
packageName: String,
displayName: String,
status: Int
) {
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification = NotificationUtil.getInstallerStatusNotification(
@@ -109,23 +114,24 @@ class InstallerStatusReceiver : BroadcastReceiver() {
private fun postStatus(status: Int, packageName: String?, extra: String?, context: Context) {
val event = when (status) {
PackageInstaller.STATUS_SUCCESS -> {
InstallerEvent.Success(
packageName, context.getString(R.string.installer_status_success)
)
InstallerEvent.Installed(packageName!!).apply {
this.extra = context.getString(R.string.installer_status_success)
}
}
PackageInstaller.STATUS_FAILURE_ABORTED -> {
InstallerEvent.Cancelled(
packageName, AppInstaller.getErrorString(context, status)
)
InstallerEvent.Cancelled(packageName!!).apply {
this.extra = AppInstaller.getErrorString(context, status)
}
}
else -> {
InstallerEvent.Failed(
packageName, AppInstaller.getErrorString(context, status), extra
)
InstallerEvent.Failed(packageName!!).apply {
this.error = AppInstaller.getErrorString(context, status)
this.extra = extra ?: ""
}
}
}
AuroraApp.flowEvent.emitEvent(event)
AuroraApp.events.send(event)
}
}

View File

@@ -23,8 +23,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.aurora.store.AuroraApp
import com.aurora.store.data.event.BusEvent.InstallEvent
import com.aurora.store.data.event.BusEvent.UninstallEvent
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.installer.AppInstaller
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@@ -41,11 +40,11 @@ open class PackageManagerReceiver : BroadcastReceiver() {
when (intent.action) {
Intent.ACTION_PACKAGE_ADDED -> {
AuroraApp.flowEvent.emitEvent(InstallEvent(packageName, ""))
AuroraApp.events.send(InstallerEvent.Installed(packageName))
}
Intent.ACTION_PACKAGE_REMOVED -> {
AuroraApp.flowEvent.emitEvent(UninstallEvent(packageName, ""))
AuroraApp.events.send(InstallerEvent.Uninstalled(packageName))
}
}

View File

@@ -25,7 +25,6 @@ import android.util.AttributeSet
import android.widget.RelativeLayout
import androidx.core.content.ContextCompat
import com.aurora.extensions.getString
import com.aurora.extensions.runOnUiThread
import com.aurora.store.R
import com.aurora.store.data.model.State
import com.aurora.store.databinding.ViewActionButtonBinding
@@ -101,19 +100,21 @@ class ActionButton : RelativeLayout {
binding.btn.text = getString(text)
}
fun setButtonState(enabled: Boolean = true) {
binding.btn.isEnabled = enabled
}
fun updateState(state: State) {
val displayChild = when (state) {
State.IDLE -> 0
State.PROGRESS -> 1
State.COMPLETE -> 2
else -> 0
}
if (binding.viewFlipper.displayedChild != displayChild) {
runOnUiThread {
binding.viewFlipper.displayedChild = displayChild
if (displayChild == 2) updateState(State.IDLE)
}
binding.viewFlipper.displayedChild = displayChild
if (displayChild == 2) updateState(State.IDLE)
}
}

View File

@@ -47,7 +47,7 @@ class CarouselModelGroup(
val models = ArrayList<EpoxyModel<*>>()
val clusterViewModels = mutableListOf<EpoxyModel<*>>()
val idPrefix = streamCluster.id
val idPrefix = streamCluster.clusterTitle.hashCode()
models.add(
HeaderViewModel_()

View File

@@ -19,13 +19,17 @@
package com.aurora.store.view.epoxy.views.app
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.core.view.isVisible
import coil.load
import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation
import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp
@@ -51,6 +55,8 @@ class AppUpdateView @JvmOverloads constructor(
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : BaseView<ViewAppUpdateBinding>(context, attrs, defStyleAttr) {
private var iconDrawable: Drawable? = null
private val cornersTransformation = RoundedCornersTransformation(8.px.toFloat())
@ModelProp
fun app(app: App) {
@@ -59,7 +65,10 @@ class AppUpdateView @JvmOverloads constructor(
binding.txtLine1.text = displayName
binding.imgIcon.load(iconArtwork.url) {
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(8.px.toFloat()))
transformations(cornersTransformation)
listener { _, result ->
result.drawable.let { iconDrawable = it }
}
}
binding.txtLine2.text = developerName
@@ -90,29 +99,22 @@ class AppUpdateView @JvmOverloads constructor(
@ModelProp
fun download(download: Download?) {
if (download != null) {
/*Inflate Download details*/
binding.btnAction.updateState(download.downloadStatus)
binding.progressDownload.isIndeterminate = download.progress < 1
when (download.downloadStatus) {
DownloadStatus.QUEUED -> {
binding.progressDownload.progress = 0
binding.progressDownload.show()
binding.progressDownload.isIndeterminate = true
animateImageView(scaleFactor = 0.75f)
}
DownloadStatus.DOWNLOADING -> {
if (download.progress > 0) {
if (download.progress == 100) {
binding.progressDownload.invisible()
} else {
binding.progressDownload.progress = download.progress
binding.progressDownload.show()
}
}
binding.progressDownload.isIndeterminate = false
binding.progressDownload.progress = download.progress
animateImageView(scaleFactor = 0.75f)
}
else -> {
binding.progressDownload.progress = 0
binding.progressDownload.invisible()
binding.progressDownload.isIndeterminate = true
animateImageView(scaleFactor = 1f)
}
}
}
@@ -141,7 +143,48 @@ class AppUpdateView @JvmOverloads constructor(
@OnViewRecycled
fun clear() {
binding.headerIndicator.removeCallbacks { }
binding.progressDownload.progress = 0
binding.progressDownload.invisible()
iconDrawable = null
}
private fun animateImageView(scaleFactor: Float = 1f) {
val isDownloadVisible = binding.progressDownload.isShown
// Avoids flickering when the download is in progress
if (isDownloadVisible && scaleFactor != 1f)
return
if (!isDownloadVisible && scaleFactor == 1f)
return
if (scaleFactor == 1f) {
binding.progressDownload.invisible()
} else {
binding.progressDownload.show()
}
val scale = listOf(
ObjectAnimator.ofFloat(binding.imgIcon, "scaleX", scaleFactor),
ObjectAnimator.ofFloat(binding.imgIcon, "scaleY", scaleFactor)
)
scale.forEach { animation ->
animation.apply {
interpolator = AccelerateDecelerateInterpolator()
duration = 250
start()
}
}
iconDrawable?.let {
binding.imgIcon.load(it) {
transformations(
if (scaleFactor == 1f)
cornersTransformation
else
CircleCropTransformation()
)
}
}
}
}

View File

@@ -34,7 +34,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.aurora.store.AuroraApp
import com.aurora.store.R
import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.event.AuthEvent
import com.aurora.store.databinding.FragmentGoogleBinding
import com.aurora.store.util.AC2DMUtil
import com.aurora.store.view.ui.commons.BaseFragment
@@ -110,15 +110,15 @@ class GoogleFragment : BaseFragment<FragmentGoogleBinding>() {
}
viewLifecycleOwner.lifecycleScope.launch {
AuroraApp.flowEvent.busEvent.collect { onEventReceived(it) }
AuroraApp.events.authEvent.collect { onEventReceived(it) }
}
}
private fun onEventReceived(event: BusEvent) {
private fun onEventReceived(event: AuthEvent) {
when (event) {
is BusEvent.GoogleAAS -> {
is AuthEvent.GoogleLogin -> {
if (event.success) {
viewModel.buildGoogleAuthData(event.email, event.aasToken)
viewModel.buildGoogleAuthData(event.email, event.token)
} else {
Toast.makeText(
requireContext(),

View File

@@ -65,8 +65,6 @@ class AppsGamesFragment : BaseFragment<FragmentGenericWithPagerBinding>() {
) { tab: TabLayout.Tab, position: Int ->
when (position) {
0 -> tab.text = getString(R.string.title_installed)
1 -> tab.text = getString(R.string.title_library)
2 -> tab.text = getString(R.string.title_purchase_history)
else -> {}
}
}.attach()
@@ -81,14 +79,12 @@ class AppsGamesFragment : BaseFragment<FragmentGenericWithPagerBinding>() {
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> InstalledAppsFragment.newInstance()
1 -> LibraryAppsFragment.newInstance()
2 -> PurchasedAppsFragment.newInstance()
else -> Fragment()
}
}
override fun getItemCount(): Int {
return if (isAnonymous) 1 else 3
return 1
}
}
}

View File

@@ -27,6 +27,8 @@ import androidx.lifecycle.lifecycleScope
import com.aurora.gplayapi.data.models.App
import com.aurora.store.AuroraApp
import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.event.Event
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.databinding.FragmentAppsBinding
import com.aurora.store.view.epoxy.views.HeaderViewModel_
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
@@ -49,10 +51,10 @@ class InstalledAppsFragment : BaseFragment<FragmentAppsBinding>() {
}
}
private fun onEvent(event: BusEvent) {
private fun onEvent(event: Event) {
when (event) {
is BusEvent.InstallEvent,
is BusEvent.UninstallEvent,
is InstallerEvent.Installed,
is InstallerEvent.Uninstalled,
is BusEvent.Blacklisted -> {
viewModel.fetchApps()
}
@@ -73,7 +75,7 @@ class InstalledAppsFragment : BaseFragment<FragmentAppsBinding>() {
}
viewLifecycleOwner.lifecycleScope.launch {
AuroraApp.flowEvent.busEvent.collect { onEvent(it) }
AuroraApp.events.busEvent.collect { onEvent(it) }
}
}

View File

@@ -1,106 +0,0 @@
/*
* 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.ui.all
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.R
import com.aurora.store.databinding.FragmentAppsBinding
import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener
import com.aurora.store.view.epoxy.views.AppProgressViewModel_
import com.aurora.store.view.epoxy.views.HeaderViewModel_
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.all.LibraryAppsViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class LibraryAppsFragment : BaseFragment<FragmentAppsBinding>() {
private val viewModel: LibraryAppsViewModel by viewModels()
companion object {
@JvmStatic
fun newInstance(): LibraryAppsFragment {
return LibraryAppsFragment().apply {
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val endlessRecyclerOnScrollListener = object : EndlessRecyclerOnScrollListener() {
override fun onLoadMore(currentPage: Int) {
viewModel.observe()
}
}
binding.recycler.addOnScrollListener(endlessRecyclerOnScrollListener)
viewModel.liveData.observe(viewLifecycleOwner) {
updateController(it)
}
updateController(null)
}
private fun updateController(streamCluster: StreamCluster?) {
binding.recycler.withModels {
setFilterDuplicates(true)
if (streamCluster == null) {
for (i in 1..10) {
add(
AppListViewShimmerModel_()
.id(i)
)
}
} else {
add(
HeaderViewModel_()
.id("header")
.title(
if (streamCluster.clusterTitle.isEmpty())
getString(R.string.title_apps_library)
else
streamCluster.clusterTitle
)
)
streamCluster.clusterAppList.forEach { app ->
add(
AppListViewModel_()
.id(app.id)
.app(app)
.click { _ -> openDetailsFragment(app.packageName, app) }
)
}
if (streamCluster.hasNext()) {
add(
AppProgressViewModel_()
.id("progress")
)
}
}
}
}
}

View File

@@ -1,98 +0,0 @@
/*
* 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.ui.all
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import com.aurora.store.R
import com.aurora.store.databinding.FragmentAppsBinding
import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener
import com.aurora.store.view.epoxy.views.AppProgressViewModel_
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.all.PaginatedAppList
import com.aurora.store.viewmodel.all.PurchasedViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class PurchasedAppsFragment : BaseFragment<FragmentAppsBinding>() {
private val viewModel: PurchasedViewModel by viewModels()
companion object {
@JvmStatic
fun newInstance(): PurchasedAppsFragment {
return PurchasedAppsFragment()
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val endlessRecyclerOnScrollListener = object : EndlessRecyclerOnScrollListener(8) {
override fun onLoadMore(currentPage: Int) {
viewModel.observe()
}
}
binding.recycler.addOnScrollListener(endlessRecyclerOnScrollListener)
viewModel.liveData.observe(viewLifecycleOwner) {
updateController(it)
}
updateController(null)
}
private fun updateController(paginatedAppList: PaginatedAppList?) {
binding.recycler.withModels {
setFilterDuplicates(true)
if (paginatedAppList == null) {
for (i in 1..10) {
add(
AppListViewShimmerModel_()
.id(i)
)
}
} else {
paginatedAppList.appList.forEach { app ->
add(
AppListViewModel_()
.id(app.id)
.app(app)
.click { _ -> openDetailsFragment(app.packageName, app) }
.longClick { _ ->
openAppMenuSheet(app)
false
}
)
}
if (paginatedAppList.hasMore) {
add(
AppProgressViewModel_()
.id("progress")
)
}
}
}
}
}

View File

@@ -33,7 +33,6 @@ import com.aurora.store.databinding.FragmentAppsGamesBinding
import com.aurora.store.util.Preferences
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.view.ui.commons.CategoryFragment
import com.aurora.store.view.ui.commons.EditorChoiceFragment
import com.aurora.store.view.ui.commons.ForYouFragment
import com.aurora.store.view.ui.commons.TopChartContainerFragment
import com.google.android.material.tabs.TabLayout
@@ -92,10 +91,6 @@ class AppsContainerFragment : BaseFragment<FragmentAppsGamesBinding>() {
add(getString(R.string.tab_top_charts))
add(getString(R.string.tab_categories))
if (!authProvider.isAnonymous) {
add(getString(R.string.tab_editor_choice))
}
}
TabLayoutMediator(
@@ -130,10 +125,6 @@ class AppsContainerFragment : BaseFragment<FragmentAppsGamesBinding>() {
}
add(TopChartContainerFragment.newInstance(0))
add(CategoryFragment.newInstance(0))
if (isGoogleAccount) {
add(EditorChoiceFragment.newInstance(0))
}
}
override fun createFragment(position: Int): Fragment {

View File

@@ -28,6 +28,7 @@ import androidx.navigation.fragment.findNavController
import androidx.viewbinding.ViewBinding
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Category
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.MobileNavigationDirections
import com.google.gson.Gson
import java.lang.reflect.ParameterizedType
@@ -93,19 +94,12 @@ abstract class BaseFragment<ViewBindingType : ViewBinding> : Fragment() {
title
)
)
} else {
findNavController().navigate(
MobileNavigationDirections.actionGlobalStreamBrowseFragment(
browseUrl,
title
)
)
}
}
fun openEditorStreamBrowseFragment(browseUrl: String, title: String = "") {
fun openStreamBrowseFragment(streamCluster: StreamCluster) {
findNavController().navigate(
MobileNavigationDirections.actionGlobalEditorStreamBrowseFragment(title, browseUrl)
MobileNavigationDirections.actionGlobalStreamBrowseFragment(streamCluster)
)
}

View File

@@ -28,20 +28,21 @@ import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.helpers.contracts.StreamContract
import com.aurora.gplayapi.utils.CategoryUtil
import com.aurora.store.data.model.ViewState
import com.aurora.store.data.model.ViewState.Loading.getDataAs
import com.aurora.store.databinding.FragmentGenericWithToolbarBinding
import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener
import com.aurora.store.view.epoxy.controller.CategoryCarouselController
import com.aurora.store.view.epoxy.controller.GenericCarouselController
import com.aurora.store.viewmodel.subcategory.SubCategoryClusterViewModel
import com.aurora.store.viewmodel.subcategory.CategoryStreamViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class CategoryBrowseFragment : BaseFragment<FragmentGenericWithToolbarBinding>(),
GenericCarouselController.Callbacks {
private val args: CategoryBrowseFragmentArgs by navArgs()
private val viewModel: SubCategoryClusterViewModel by activityViewModels()
private val viewModel: CategoryStreamViewModel by activityViewModels()
private lateinit var category: StreamContract.Category
private var streamBundle: StreamBundle? = StreamBundle()
@@ -49,8 +50,7 @@ class CategoryBrowseFragment : BaseFragment<FragmentGenericWithToolbarBinding>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val rawCategory = args.browseUrl.split("/").last()
category = StreamContract.Category.NONE.apply { value = rawCategory }
category = CategoryUtil.getCategoryFromUrl(args.browseUrl)
val genericCarouselController = CategoryCarouselController(this)
@@ -68,6 +68,7 @@ class CategoryBrowseFragment : BaseFragment<FragmentGenericWithToolbarBinding>()
}
})
viewModel.getStreamBundle(category)
viewModel.liveData.observe(viewLifecycleOwner) {
when (it) {
is ViewState.Loading -> {
@@ -84,12 +85,10 @@ class CategoryBrowseFragment : BaseFragment<FragmentGenericWithToolbarBinding>()
else -> {}
}
}
viewModel.observe(category)
}
override fun onHeaderClicked(streamCluster: StreamCluster) {
openStreamBrowseFragment(streamCluster)
}
override fun onClusterScrolled(streamCluster: StreamCluster) {

View File

@@ -1,77 +0,0 @@
/*
* 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.ui.commons
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import com.aurora.Constants
import com.aurora.gplayapi.data.models.editor.EditorChoiceCluster
import com.aurora.gplayapi.helpers.contracts.StreamContract
import com.aurora.store.databinding.FragmentForYouBinding
import com.aurora.store.view.epoxy.controller.EditorChoiceController
import com.aurora.store.viewmodel.editorschoice.EditorChoiceViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class EditorChoiceFragment : BaseFragment<FragmentForYouBinding>(),
EditorChoiceController.Callbacks {
private val viewModel: EditorChoiceViewModel by viewModels()
companion object {
@JvmStatic
fun newInstance(pageType: Int): EditorChoiceFragment {
return EditorChoiceFragment().apply {
arguments = Bundle().apply {
putInt(Constants.PAGE_TYPE, pageType)
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
var pageType = 0
val bundle = arguments
if (bundle != null) {
pageType = bundle.getInt(Constants.PAGE_TYPE, 0)
}
val editorChoiceController = EditorChoiceController(this)
binding.recycler.setController(editorChoiceController)
when (pageType) {
0 -> viewModel.getEditorChoiceStream(StreamContract.Category.APPLICATION)
1 -> viewModel.getEditorChoiceStream(StreamContract.Category.GAME)
}
viewModel.liveData.observe(viewLifecycleOwner) {
editorChoiceController.setData(it)
}
}
override fun onClick(editorChoiceCluster: EditorChoiceCluster) {
openEditorStreamBrowseFragment(
editorChoiceCluster.clusterBrowseUrl,
editorChoiceCluster.clusterTitle
)
}
}

View File

@@ -1,139 +0,0 @@
/*
* 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.ui.commons
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.airbnb.epoxy.EpoxyModel
import com.aurora.gplayapi.data.models.App
import com.aurora.store.databinding.FragmentGenericWithToolbarBinding
import com.aurora.store.view.epoxy.groups.CarouselHorizontalModel_
import com.aurora.store.view.epoxy.views.EditorHeadViewModel_
import com.aurora.store.view.epoxy.views.HorizontalDividerViewModel_
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
import com.aurora.store.view.epoxy.views.details.MiniScreenshotView
import com.aurora.store.view.epoxy.views.details.MiniScreenshotViewModel_
import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_
import com.aurora.store.viewmodel.editorschoice.EditorBrowseViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class EditorStreamBrowseFragment : BaseFragment<FragmentGenericWithToolbarBinding>() {
private val args: EditorStreamBrowseFragmentArgs by navArgs()
private val viewModel: EditorBrowseViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Toolbar
binding.layoutToolbarAction.apply {
txtTitle.text = args.title
imgActionPrimary.setOnClickListener {
findNavController().navigateUp()
}
}
viewModel.liveData.observe(viewLifecycleOwner) {
updateController(it)
}
viewModel.getEditorStreamBundle(args.browseUrl)
updateController(null)
}
private fun updateController(appList: MutableList<App>?) {
binding.recycler.withModels {
setFilterDuplicates(true)
if (appList == null) {
for (i in 1..6) {
add(
AppListViewShimmerModel_()
.id(i)
)
}
} else {
appList.forEach { app ->
val screenshotsViewModels = mutableListOf<EpoxyModel<*>>()
for ((position, artwork) in app.screenshots.withIndex()) {
screenshotsViewModels.add(
MiniScreenshotViewModel_()
.id(artwork.url)
.position(position)
.artwork(artwork)
.callback(object : MiniScreenshotView.ScreenshotCallback {
override fun onClick(position: Int) {
openScreenshotFragment(app, position)
}
})
)
}
add(
AppListViewModel_()
.id("app_${app.id}")
.app(app)
.click { _ -> openDetailsFragment(app.packageName, app) }
)
app.editorReason?.let { editorReason ->
add(
EditorHeadViewModel_()
.id("bulletin_${app.id}")
.title(
editorReason.bulletins
.joinToString(transform = { "\n$it" })
.substringAfter(delimiter = "\n")
)
.click { _ -> openDetailsFragment(app.packageName, app) }
)
}
if (screenshotsViewModels.isNotEmpty()) {
add(
CarouselHorizontalModel_()
.id("screenshots_${app.id}")
.models(screenshotsViewModels)
)
}
app.editorReason?.let { editorReason ->
if (editorReason.description.isNotEmpty()) {
add(
EditorHeadViewModel_()
.id("description_${app.id}")
.title(editorReason.description)
.click { _ -> openDetailsFragment(app.packageName, app) }
)
}
}
add(
HorizontalDividerViewModel_()
.id("divider_${app.id}")
)
}
}
}
}
}

View File

@@ -67,27 +67,18 @@ class ForYouFragment : BaseFragment<FragmentForYouBinding>(),
pageType = bundle.getInt(Constants.PAGE_TYPE, 0)
}
when (pageType) {
0 -> {
category = Category.APPLICATION
viewModel.getStreamBundle(category, Type.HOME)
}
1 -> {
category = Category.GAME
viewModel.getStreamBundle(category, Type.HOME)
}
}
category = if (pageType == 0) Category.APPLICATION else Category.GAME
binding.recycler.setController(genericCarouselController)
binding.recycler.addOnScrollListener(
object : EndlessRecyclerOnScrollListener() {
object : EndlessRecyclerOnScrollListener(visibleThreshold = 4) {
override fun onLoadMore(currentPage: Int) {
viewModel.observe(category, Type.HOME)
}
}
)
viewModel.getStreamBundle(category, Type.HOME)
viewModel.liveData.observe(viewLifecycleOwner) {
when (it) {
is ViewState.Loading -> {
@@ -107,7 +98,7 @@ class ForYouFragment : BaseFragment<FragmentForYouBinding>(),
}
override fun onHeaderClicked(streamCluster: StreamCluster) {
openStreamBrowseFragment(streamCluster)
}
override fun onClusterScrolled(streamCluster: StreamCluster) {

View File

@@ -270,11 +270,6 @@ class MoreDialogFragment : DialogFragment() {
icon = R.drawable.ic_apps,
destinationID = R.id.appsGamesFragment
),
Option(
title = R.string.title_apps_sale,
icon = R.drawable.ic_sale,
destinationID = R.id.appSalesFragment
),
Option(
title = R.string.title_blacklist_manager,
icon = R.drawable.ic_blacklist,

View File

@@ -38,38 +38,32 @@ class StreamBrowseFragment : BaseFragment<FragmentGenericWithToolbarBinding>() {
private val args: StreamBrowseFragmentArgs by navArgs()
private val viewModel: StreamBrowseViewModel by viewModels()
private lateinit var endlessRecyclerOnScrollListener: EndlessRecyclerOnScrollListener
private lateinit var cluster: StreamCluster
private lateinit var streamCluster: StreamCluster
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
streamCluster = args.cluster
// Toolbar
binding.layoutToolbarAction.apply {
txtTitle.text = args.title
txtTitle.text = streamCluster.clusterTitle
imgActionPrimary.setOnClickListener {
findNavController().navigateUp()
}
}
viewModel.liveData.observe(viewLifecycleOwner) {
if (!::cluster.isInitialized) {
endlessRecyclerOnScrollListener = object : EndlessRecyclerOnScrollListener() {
override fun onLoadMore(currentPage: Int) {
viewModel.nextCluster()
}
}
binding.recycler.addOnScrollListener(endlessRecyclerOnScrollListener)
binding.recycler.addOnScrollListener(object :
EndlessRecyclerOnScrollListener(visibleThreshold = 4) {
override fun onLoadMore(currentPage: Int) {
viewModel.nextCluster()
}
})
cluster = it
updateController(cluster)
binding.layoutToolbarAction.txtTitle.text = it.clusterTitle
viewModel.initCluster(streamCluster)
viewModel.liveData.observe(viewLifecycleOwner) {
updateController(streamCluster)
}
viewModel.getStreamBundle(args.browseUrl)
updateController(null)
}
private fun updateController(streamCluster: StreamCluster?) {

View File

@@ -55,8 +55,8 @@ import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.AuroraApp
import com.aurora.store.AppStreamStash
import com.aurora.store.AuroraApp
import com.aurora.store.R
import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.event.Event
@@ -135,7 +135,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
private fun onEvent(event: Event) {
when (event) {
is BusEvent.InstallEvent -> {
is InstallerEvent.Installed -> {
if (app.packageName == event.packageName) {
attachActions()
binding.layoutDetailsToolbar.toolbar.menu.apply {
@@ -147,7 +147,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
}
}
is BusEvent.UninstallEvent -> {
is InstallerEvent.Uninstalled -> {
if (app.packageName == event.packageName) {
attachActions()
binding.layoutDetailsToolbar.toolbar.menu.apply {
@@ -166,14 +166,23 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
}
is InstallerEvent.Failed -> {
findNavController().navigate(
AppDetailsFragmentDirections.actionAppDetailsFragmentToInstallErrorDialogSheet(
app,
event.packageName ?: "",
event.error ?: "",
event.extra ?: ""
if (app.packageName == event.packageName) {
findNavController().navigate(
AppDetailsFragmentDirections.actionAppDetailsFragmentToInstallErrorDialogSheet(
app,
event.packageName ?: "",
event.error ?: "",
event.extra ?: ""
)
)
)
}
}
is InstallerEvent.Installing -> {
if (event.packageName == app.packageName) {
attachActions()
updateActionState(State.INSTALLING)
}
}
else -> {
@@ -356,10 +365,10 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
}
viewLifecycleOwner.lifecycleScope.launch {
AuroraApp.flowEvent.busEvent.collect { onEvent(it) }
AuroraApp.events.busEvent.collect { onEvent(it) }
}
viewLifecycleOwner.lifecycleScope.launch {
AuroraApp.flowEvent.installerEvent.collect { onEvent(it) }
AuroraApp.events.installerEvent.collect { onEvent(it) }
}
}
@@ -507,7 +516,15 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
}
private fun updateActionState(state: State) {
binding.layoutDetailsInstall.btnDownload.updateState(state)
runOnUiThread {
binding.layoutDetailsInstall.btnDownload.apply {
updateState(state)
if (state == State.INSTALLING) {
setButtonState(false)
setText(R.string.action_installing)
}
}
}
}
private fun openApp() {
@@ -589,9 +606,8 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName)
runOnUiThread {
app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName)
binding.layoutDetailsInstall.btnDownload.let { btn ->
btn.setButtonState(true)
if (app.isInstalled) {
isUpdatable = PackageUtil.isUpdatable(
requireContext(),

View File

@@ -33,7 +33,6 @@ import com.aurora.store.databinding.FragmentAppsGamesBinding
import com.aurora.store.util.Preferences
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.view.ui.commons.CategoryFragment
import com.aurora.store.view.ui.commons.EditorChoiceFragment
import com.aurora.store.view.ui.commons.ForYouFragment
import com.aurora.store.view.ui.commons.TopChartContainerFragment
import com.google.android.material.tabs.TabLayout
@@ -91,10 +90,6 @@ class GamesContainerFragment : BaseFragment<FragmentAppsGamesBinding>() {
add(getString(R.string.tab_top_charts))
add(getString(R.string.tab_categories))
if (!authProvider.isAnonymous) {
add(getString(R.string.tab_editor_choice))
}
}
TabLayoutMediator(
@@ -129,10 +124,6 @@ class GamesContainerFragment : BaseFragment<FragmentAppsGamesBinding>() {
add(TopChartContainerFragment.newInstance(1))
add(CategoryFragment.newInstance(1))
if (isGoogleAccount) {
add(EditorChoiceFragment.newInstance(1))
}
}
override fun createFragment(position: Int): Fragment {

View File

@@ -1,113 +0,0 @@
/*
* 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.ui.sale
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.aurora.store.R
import com.aurora.store.databinding.FragmentGenericWithToolbarBinding
import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener
import com.aurora.store.view.epoxy.views.AppProgressViewModel_
import com.aurora.store.view.epoxy.views.HeaderViewModel_
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
import com.aurora.store.view.epoxy.views.app.NoAppViewModel_
import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.all.PaginatedAppList
import com.aurora.store.viewmodel.sale.AppSalesViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class AppSalesFragment : BaseFragment<FragmentGenericWithToolbarBinding>() {
private val viewModel: AppSalesViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.layoutToolbarAction.txtTitle.text = getString(R.string.title_apps_sale)
binding.layoutToolbarAction.imgActionPrimary.setOnClickListener {
findNavController().navigateUp()
}
val endlessRecyclerOnScrollListener = object : EndlessRecyclerOnScrollListener() {
override fun onLoadMore(currentPage: Int) {
viewModel.observe()
}
}
binding.recycler.addOnScrollListener(endlessRecyclerOnScrollListener)
updateController(null)
viewModel.liveData.observe(viewLifecycleOwner) {
updateController(it)
}
}
private fun updateController(paginatedAppList: PaginatedAppList?) {
binding.recycler
.withModels {
setFilterDuplicates(true)
if (paginatedAppList == null) {
for (i in 1..6) {
add(
AppListViewShimmerModel_()
.id(i)
)
}
} else {
add(
HeaderViewModel_()
.id("header")
.title(getString(R.string.title_apps_sale_provider))
)
paginatedAppList.appList
.filter { it.packageName.isNotEmpty() }
.forEach {
add(
AppListViewModel_()
.id(it.packageName.hashCode())
.app(it)
.click { _ -> openDetailsFragment(it.packageName, it) }
)
setFilterDuplicates(true)
}
if (paginatedAppList.hasMore) {
add(
AppProgressViewModel_()
.id("progress")
)
}
if (!paginatedAppList.hasMore && paginatedAppList.appList.isEmpty()) {
add(
NoAppViewModel_()
.id("no_app_sale")
.icon(R.drawable.ic_apps)
.message(getString(R.string.details_no_apps_on_sale))
)
}
}
}
}
}

View File

@@ -94,8 +94,8 @@ class AppMenuSheet : BaseDialogSheet<SheetAppMenuBinding>() {
}
dismissAllowingStateLoss()
AuroraApp.flowEvent.emitEvent(
BusEvent.Blacklisted(args.app.packageName, "")
AuroraApp.events.send(
BusEvent.Blacklisted(args.app.packageName)
)
}

View File

@@ -37,7 +37,8 @@ import com.aurora.gplayapi.data.models.App
import com.aurora.store.AuroraApp
import com.aurora.store.MobileNavigationDirections
import com.aurora.store.R
import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.event.Event
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.room.download.Download
import com.aurora.store.databinding.FragmentUpdatesBinding
import com.aurora.store.util.PackageUtil
@@ -143,13 +144,13 @@ class UpdatesFragment : BaseFragment<FragmentUpdatesBinding>() {
}
viewLifecycleOwner.lifecycleScope.launch {
AuroraApp.flowEvent.busEvent.collect { onEvent(it) }
AuroraApp.events.busEvent.collect { onEvent(it) }
}
}
private fun onEvent(event: BusEvent) {
private fun onEvent(event: Event) {
when (event) {
is BusEvent.InstallEvent, is BusEvent.UninstallEvent -> {
is InstallerEvent.Installed, is InstallerEvent.Uninstalled -> {
viewModel.fetchUpdates()
}

View File

@@ -1,81 +0,0 @@
/*
* 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.viewmodel.all
import android.annotation.SuppressLint
import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.helpers.ClusterHelper
import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AuthProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import javax.inject.Inject
@HiltViewModel
@SuppressLint("StaticFieldLeak") // false positive, see https://github.com/google/dagger/issues/3253
class LibraryAppsViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val authProvider: AuthProvider
) : ViewModel() {
private val clusterHelper: ClusterHelper = ClusterHelper(authProvider.authData)
.using(HttpClient.getPreferredClient(context))
val liveData: MutableLiveData<StreamCluster> = MutableLiveData()
var streamCluster: StreamCluster = StreamCluster()
fun observe() {
viewModelScope.launch(Dispatchers.IO) {
supervisorScope {
try {
when {
streamCluster.clusterAppList.isEmpty() -> {
val newCluster =
clusterHelper.getCluster(ClusterHelper.Type.MY_APPS_LIBRARY)
updateCluster(newCluster)
liveData.postValue(streamCluster)
}
streamCluster.hasNext() -> {
val newCluster = clusterHelper.next(streamCluster.clusterNextPageUrl)
updateCluster(newCluster)
liveData.postValue(streamCluster)
}
else -> {}
}
} catch (_: Exception) {
}
}
}
}
private fun updateCluster(newCluster: StreamCluster) {
streamCluster.apply {
clusterAppList.addAll(newCluster.clusterAppList)
clusterNextPageUrl = newCluster.clusterNextPageUrl
}
}
}

View File

@@ -1,83 +0,0 @@
/*
* 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.viewmodel.all
import android.annotation.SuppressLint
import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.helpers.PurchaseHelper
import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AuthProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import javax.inject.Inject
data class PaginatedAppList(
var appList: MutableList<App> = mutableListOf(),
var hasMore: Boolean
)
@HiltViewModel
@SuppressLint("StaticFieldLeak") // false positive, see https://github.com/google/dagger/issues/3253
class PurchasedViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val authProvider: AuthProvider
) : ViewModel() {
private val purchaseHelper = PurchaseHelper(authProvider.authData)
.using(HttpClient.getPreferredClient(context))
private var appList: MutableList<App> = mutableListOf()
val liveData: MutableLiveData<PaginatedAppList> = MutableLiveData()
fun observe() {
viewModelScope.launch(Dispatchers.IO) {
supervisorScope {
try {
val nextAppList = purchaseHelper.getPurchaseHistory(appList.size)
.filter { it.displayName.isNotEmpty() }
if (nextAppList.isEmpty()) {
liveData.postValue(PaginatedAppList(
appList,
false
))
} else {
appList.addAll(nextAppList)
liveData.postValue(
PaginatedAppList(
appList,
true
)
)
}
} catch (_: Exception) {
}
}
}
}
}

View File

@@ -32,7 +32,7 @@ import com.aurora.gplayapi.data.providers.DeviceInfoProvider
import com.aurora.gplayapi.helpers.AuthHelper
import com.aurora.store.AuroraApp
import com.aurora.store.R
import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.event.AuthEvent
import com.aurora.store.data.model.AccountType
import com.aurora.store.data.model.AuthState
import com.aurora.store.data.model.InsecureAuth
@@ -201,18 +201,18 @@ class AuthViewModel @Inject constructor(
if (aasToken != null) {
Preferences.putString(context, Constants.ACCOUNT_EMAIL_PLAIN, email)
Preferences.putString(context, Constants.ACCOUNT_AAS_PLAIN, aasToken)
AuroraApp.flowEvent.emitEvent(BusEvent.GoogleAAS(true, email, aasToken))
AuroraApp.events.send(AuthEvent.GoogleLogin(true, email, aasToken))
} else {
Preferences.putString(context, Constants.ACCOUNT_EMAIL_PLAIN, "")
Preferences.putString(context, Constants.ACCOUNT_AAS_PLAIN, "")
AuroraApp.flowEvent.emitEvent(BusEvent.GoogleAAS(false))
AuroraApp.events.send(AuthEvent.GoogleLogin(false, "", ""))
}
} else {
AuroraApp.flowEvent.emitEvent(BusEvent.GoogleAAS(false))
AuroraApp.events.send(AuthEvent.GoogleLogin(false, "", ""))
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to build AuthData", exception)
AuroraApp.flowEvent.emitEvent(BusEvent.GoogleAAS(false))
AuroraApp.events.send(AuthEvent.GoogleLogin(false, "", ""))
}
}
}
@@ -238,6 +238,7 @@ class AuthViewModel @Inject constructor(
)
buildGoogleAuthData(email, aasToken)
}
AccountType.ANONYMOUS -> {
buildAnonymousAuthData()
}
@@ -248,9 +249,11 @@ class AuthViewModel @Inject constructor(
is UnknownHostException -> {
context.getString(R.string.title_no_network)
}
is ConnectException -> {
context.getString(R.string.server_unreachable)
}
else -> {
context.getString(R.string.bad_request)
}

View File

@@ -25,7 +25,8 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.helpers.StreamHelper
import com.aurora.gplayapi.helpers.contracts.StreamContract
import com.aurora.gplayapi.helpers.web.WebStreamHelper
import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.util.Log
@@ -43,31 +44,16 @@ class StreamBrowseViewModel @Inject constructor(
private val authProvider: AuthProvider
) : ViewModel() {
private val streamHelper: StreamHelper = StreamHelper(authProvider.authData)
private val streamHelper: StreamContract = WebStreamHelper()
.using(HttpClient.getPreferredClient(context))
val liveData: MutableLiveData<StreamCluster> = MutableLiveData()
var streamCluster: StreamCluster = StreamCluster()
fun getStreamBundle(browseUrl: String) {
viewModelScope.launch(Dispatchers.IO) {
liveData.postValue(getInitialCluster(browseUrl))
}
}
private lateinit var streamCluster: StreamCluster
private fun getInitialCluster(browseUrl: String): StreamCluster {
val browseResponse = streamHelper.getBrowseStreamResponse(browseUrl)
if (browseResponse.contentsUrl.isNotEmpty())
streamCluster = streamHelper.getNextStreamCluster(browseResponse.contentsUrl)
else if (browseResponse.hasBrowseTab())
streamCluster = streamHelper.getNextStreamCluster(browseResponse.browseTab.listUrl)
streamCluster.apply {
clusterTitle = browseResponse.title
}
return streamCluster
fun initCluster(cluster: StreamCluster) {
streamCluster = cluster
liveData.postValue(streamCluster)
}
fun nextCluster() {
@@ -75,18 +61,18 @@ class StreamBrowseViewModel @Inject constructor(
supervisorScope {
try {
if (streamCluster.hasNext()) {
val newCluster = streamHelper.getNextStreamCluster(
val nextCluster = streamHelper.nextStreamCluster(
streamCluster.clusterNextPageUrl
)
streamCluster.apply {
clusterAppList.addAll(newCluster.clusterAppList)
clusterNextPageUrl = newCluster.clusterNextPageUrl
clusterAppList.addAll(nextCluster.clusterAppList)
clusterNextPageUrl = nextCluster.clusterNextPageUrl
}
liveData.postValue(streamCluster)
} else {
Log.i("End of Bundle")
Log.i("End of Cluster")
}
} catch (_: Exception) {
}

View File

@@ -28,7 +28,6 @@ import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.Category
import com.aurora.gplayapi.helpers.CategoryHelper
import com.aurora.gplayapi.helpers.contracts.CategoryContract
import com.aurora.gplayapi.helpers.web.WebCategoryHelper
import com.aurora.store.CategoryStash
import com.aurora.store.data.model.ViewState
import com.aurora.store.data.network.HttpClient
@@ -50,9 +49,6 @@ class CategoryViewModel @Inject constructor(
private val categoryHelper: CategoryHelper = CategoryHelper(authProvider.authData)
.using(HttpClient.getPreferredClient(context))
private val webCategoryHelper: CategoryContract = WebCategoryHelper()
.using(HttpClient.getPreferredClient(context))
private var stash: CategoryStash = mutableMapOf(
Category.Type.APPLICATION to emptyList(),
Category.Type.GAME to emptyList()
@@ -61,11 +57,7 @@ class CategoryViewModel @Inject constructor(
val liveData = MutableLiveData<ViewState>()
private fun contract(): CategoryContract {
return if (authProvider.isAnonymous) {
webCategoryHelper
} else {
categoryHelper
}
return categoryHelper
}
fun getCategoryList(type: Category.Type) {
@@ -74,10 +66,11 @@ class CategoryViewModel @Inject constructor(
if (categories.isNotEmpty()) {
liveData.postValue(ViewState.Success(stash))
return@launch
}
try {
stash[type] = contract().getAllCategoriesList(type)
stash[type] = contract().getAllCategories(type)
liveData.postValue(ViewState.Success(stash))
} catch (exception: Exception) {
Log.e(TAG, "Failed fetching list of categories", exception)

View File

@@ -1,74 +0,0 @@
/*
* 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.viewmodel.editorschoice
import android.annotation.SuppressLint
import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.helpers.StreamHelper
import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AuthProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import javax.inject.Inject
@HiltViewModel
@SuppressLint("StaticFieldLeak") // false positive, see https://github.com/google/dagger/issues/3253
class EditorBrowseViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val authProvider: AuthProvider
) : ViewModel() {
private val streamHelper: StreamHelper = StreamHelper(authProvider.authData)
.using(HttpClient.getPreferredClient(context))
val liveData: MutableLiveData<MutableList<App>> = MutableLiveData()
val appList: MutableList<App> = mutableListOf()
fun getEditorStreamBundle(browseUrl: String) {
viewModelScope.launch(Dispatchers.IO) {
supervisorScope {
try {
val browseResponse = streamHelper.getBrowseStreamResponse(browseUrl)
val listResponse =
streamHelper.getNextStreamResponse(browseResponse.browseTab.listUrl)
listResponse.itemList.forEach {
it?.let {
it.subItemList.forEach {
appList.addAll(streamHelper.getAppsFromItem(it))
}
}
}
liveData.postValue(appList)
} catch (_: Exception) {
}
}
}
}
}

View File

@@ -1,66 +0,0 @@
/*
* 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.viewmodel.editorschoice
import android.annotation.SuppressLint
import android.content.Context
import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.editor.EditorChoiceBundle
import com.aurora.gplayapi.helpers.StreamHelper
import com.aurora.gplayapi.helpers.contracts.StreamContract
import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AuthProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import javax.inject.Inject
@HiltViewModel
@SuppressLint("StaticFieldLeak") // false positive, see https://github.com/google/dagger/issues/3253
class EditorChoiceViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val authProvider: AuthProvider
) : ViewModel() {
private val TAG = EditorChoiceViewModel::class.java.simpleName
private val streamHelper: StreamHelper = StreamHelper(authProvider.authData)
.using(HttpClient.getPreferredClient(context))
val liveData: MutableLiveData<List<EditorChoiceBundle>> = MutableLiveData()
fun getEditorChoiceStream(category: StreamContract.Category) {
viewModelScope.launch(Dispatchers.IO) {
supervisorScope {
try {
val editorChoiceBundle = streamHelper.getEditorChoiceStream(category)
liveData.postValue(editorChoiceBundle)
} catch (exception: Exception) {
Log.e(TAG, "Failed fetching list of categories", exception)
}
}
}
}
}

View File

@@ -26,13 +26,11 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.helpers.StreamHelper
import com.aurora.gplayapi.helpers.contracts.StreamContract
import com.aurora.gplayapi.helpers.web.WebStreamHelper
import com.aurora.store.HomeStash
import com.aurora.store.data.model.ViewState
import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.util.Log
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -44,14 +42,10 @@ import javax.inject.Inject
@HiltViewModel
@SuppressLint("StaticFieldLeak") // false positive, see https://github.com/google/dagger/issues/3253
class StreamViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val authProvider: AuthProvider
@ApplicationContext private val context: Context
) : ViewModel() {
private var streamHelper: StreamContract = StreamHelper(authProvider.authData)
.using(HttpClient.getPreferredClient(context))
private var webStreamHelper: StreamContract = WebStreamHelper()
private var webStreamHelper = WebStreamHelper()
.using(HttpClient.getPreferredClient(context))
val liveData: MutableLiveData<ViewState> = MutableLiveData()
@@ -59,11 +53,7 @@ class StreamViewModel @Inject constructor(
private var stash: HomeStash = mutableMapOf()
fun contract(): StreamContract {
return if (authProvider.isAnonymous) {
webStreamHelper
} else {
streamHelper
}
return webStreamHelper
}
fun getStreamBundle(category: StreamContract.Category, type: StreamContract.Type) {
@@ -75,7 +65,7 @@ class StreamViewModel @Inject constructor(
viewModelScope.launch(Dispatchers.IO) {
supervisorScope {
val bundle = targetBundle(category)
if (bundle.streamClusters.isNotEmpty()) {
if (bundle.hasCluster()) {
liveData.postValue(ViewState.Success(stash))
}
@@ -83,13 +73,13 @@ class StreamViewModel @Inject constructor(
if (!bundle.hasCluster() || bundle.hasNext()) {
//Fetch new stream bundle
val newBundle = if (bundle.streamClusters.isEmpty()) {
contract().fetch(type, category)
} else {
val newBundle = if (bundle.hasCluster()) {
contract().nextStreamBundle(
category,
bundle.streamNextPageUrl
)
} else {
contract().fetch(type, category)
}
//Update old bundle
@@ -142,7 +132,7 @@ class StreamViewModel @Inject constructor(
}
private fun targetBundle(category: StreamContract.Category): StreamBundle {
val streamBundle = stash.getOrPut(category){
val streamBundle = stash.getOrPut(category) {
StreamBundle()
}

View File

@@ -1,95 +0,0 @@
/*
* 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.viewmodel.sale
import android.annotation.SuppressLint
import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.helpers.AppSalesHelper
import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.viewmodel.all.PaginatedAppList
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import javax.inject.Inject
@HiltViewModel
@SuppressLint("StaticFieldLeak") // false positive, see https://github.com/google/dagger/issues/3253
class AppSalesViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val authProvider: AuthProvider
) : ViewModel() {
private val appSalesHelper: AppSalesHelper = AppSalesHelper(authProvider.authData)
.using(HttpClient.getPreferredClient(context))
private var page: Int = 0
private val appList: MutableList<App> = mutableListOf()
val liveData: MutableLiveData<PaginatedAppList> = MutableLiveData()
init {
observe()
}
fun observe() {
viewModelScope.launch(Dispatchers.IO) {
supervisorScope {
try {
val nextAppList = getSearchResults()
if (nextAppList.isEmpty()) {
liveData.postValue(
PaginatedAppList(
appList,
hasMore = false
)
)
} else {
appList.addAll(nextAppList)
liveData.postValue(
PaginatedAppList(
appList,
hasMore = true
)
)
}
} catch (_: Exception) {
}
}
}
}
private fun getSearchResults(): List<App> {
return try {
appSalesHelper.getAppsOnSale(page = page++, offer = 100)
} catch (e: Exception) {
e.printStackTrace()
emptyList()
}
}
}

View File

@@ -35,7 +35,7 @@ class SheetsViewModel @Inject constructor(
val purchaseHelper = PurchaseHelper(authProvider.authData)
val files = purchaseHelper.purchase(app.packageName, customVersion, app.offerType)
if (files.isNotEmpty()) {
AuroraApp.flowEvent.emitEvent(
AuroraApp.events.send(
BusEvent.ManualDownload(app.packageName, customVersion)
)
}

View File

@@ -26,8 +26,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.helpers.contracts.CategoryStreamContract
import com.aurora.gplayapi.helpers.contracts.StreamContract
import com.aurora.gplayapi.helpers.web.WebStreamHelper
import com.aurora.gplayapi.helpers.web.WebCategoryStreamHelper
import com.aurora.store.data.model.ViewState
import com.aurora.store.data.network.HttpClient
import com.aurora.store.util.Log
@@ -40,25 +41,24 @@ import javax.inject.Inject
@HiltViewModel
@SuppressLint("StaticFieldLeak") // false positive, see https://github.com/google/dagger/issues/3253
class SubCategoryClusterViewModel @Inject constructor(
class CategoryStreamViewModel @Inject constructor(
@ApplicationContext private val context: Context
) : ViewModel() {
var contract: StreamContract = WebStreamHelper()
private var webCategoryStreamHelper = WebCategoryStreamHelper()
.using(HttpClient.getPreferredClient(context))
val liveData: MutableLiveData<ViewState> = MutableLiveData()
private var stash: MutableMap<String, StreamBundle> = mutableMapOf()
private fun getCategoryStreamBundle(
category: StreamContract.Category,
nextPageUrl: String
): StreamBundle {
return if (targetBundle(category).streamClusters.isEmpty())
contract.fetch(StreamContract.Type.HOME, category)
else
contract.nextStreamBundle(category, nextPageUrl)
fun contract(): CategoryStreamContract {
return webCategoryStreamHelper
}
fun getStreamBundle(category: StreamContract.Category) {
liveData.postValue(ViewState.Loading)
observe(category)
}
fun observe(category: StreamContract.Category) {
@@ -66,18 +66,22 @@ class SubCategoryClusterViewModel @Inject constructor(
viewModelScope.launch(Dispatchers.IO) {
supervisorScope {
val bundle = targetBundle(category)
if (bundle.streamClusters.isNotEmpty()) {
liveData.postValue(ViewState.Success(stash))
}
try {
if (!bundle.hasCluster() || bundle.hasNext()) {
//Fetch new stream bundle
val newBundle = getCategoryStreamBundle(
category,
bundle.streamNextPageUrl
)
val newBundle = if (bundle.streamClusters.isEmpty()) {
contract().fetch(category.value)
} else {
contract().nextStreamBundle(
category,
bundle.streamNextPageUrl
)
}
//Update old bundle
bundle.apply {
@@ -103,7 +107,7 @@ class SubCategoryClusterViewModel @Inject constructor(
try {
if (streamCluster.hasNext()) {
val newCluster =
contract.nextStreamCluster(streamCluster.clusterNextPageUrl)
contract().nextStreamCluster(streamCluster.clusterNextPageUrl)
updateCluster(category, streamCluster.id, newCluster)
liveData.postValue(ViewState.Success(stash))
} else {

View File

@@ -25,13 +25,11 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.helpers.TopChartsHelper
import com.aurora.gplayapi.helpers.contracts.TopChartsContract
import com.aurora.gplayapi.helpers.web.WebTopChartsHelper
import com.aurora.store.TopChartStash
import com.aurora.store.data.model.ViewState
import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AuthProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
@@ -41,13 +39,8 @@ import javax.inject.Inject
@HiltViewModel
@SuppressLint("StaticFieldLeak") // false positive, see https://github.com/google/dagger/issues/3253
class TopChartViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val authProvider: AuthProvider
) : ViewModel() {
private val topChartsHelper: TopChartsHelper = TopChartsHelper(authProvider.authData)
.using(HttpClient.getPreferredClient(context))
class TopChartViewModel @Inject constructor(@ApplicationContext private val context: Context) :
ViewModel() {
private val webTopChartsHelper: TopChartsContract = WebTopChartsHelper()
.using(HttpClient.getPreferredClient(context))
@@ -57,11 +50,7 @@ class TopChartViewModel @Inject constructor(
val liveData: MutableLiveData<ViewState> = MutableLiveData()
private fun contract(): TopChartsContract {
return if (authProvider.isAnonymous) {
webTopChartsHelper
} else {
topChartsHelper
}
return webTopChartsHelper
}
fun getStreamCluster(type: TopChartsContract.Type, chart: TopChartsContract.Chart) {
@@ -85,7 +74,7 @@ class TopChartViewModel @Inject constructor(
try {
val target = targetCluster(type, chart)
if (target.hasNext()) {
val newCluster = topChartsHelper.getNextStreamCluster(
val newCluster = contract().getNextStreamCluster(
target.clusterNextPageUrl
)

View File

@@ -52,15 +52,12 @@
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progress_download"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_width="@dimen/icon_size_medium"
android:layout_height="@dimen/icon_size_medium"
android:layout_centerInParent="true"
android:indeterminate="true"
android:indeterminateTint="@color/colorScrimAlt"
android:visibility="gone"
app:indicatorColor="@color/colorScrimAlt"
app:trackColor="@color/colorScrim"
app:trackThickness="@dimen/icon_size_default"
app:indicatorSize="@dimen/icon_size_medium"
app:trackThickness="3dp"
tools:progress="40" />
</RelativeLayout>

View File

@@ -29,9 +29,6 @@
<action
android:id="@+id/action_global_categoryBrowseFragment"
app:destination="@id/categoryBrowseFragment" />
<action
android:id="@+id/action_global_editorStreamBrowseFragment"
app:destination="@id/editorStreamBrowseFragment" />
<action
android:id="@+id/action_global_expandedStreamBrowseFragment"
app:destination="@id/expandedStreamBrowseFragment" />
@@ -78,11 +75,6 @@
android:name="com.aurora.store.view.ui.all.AppsGamesFragment"
android:label="@string/title_apps_games"
tools:layout="@layout/fragment_generic_with_pager" />
<fragment
android:id="@+id/appSalesFragment"
android:name="com.aurora.store.view.ui.sale.AppSalesFragment"
android:label="@string/title_apps_sale"
tools:layout="@layout/fragment_generic_with_toolbar" />
<fragment
android:id="@+id/spoofFragment"
android:name="com.aurora.store.view.ui.spoof.SpoofFragment"
@@ -220,16 +212,6 @@
android:id="@+id/action_categoryBrowseFragment_to_appPeekDialogSheet"
app:destination="@id/appPeekDialogSheet" />
</fragment>
<fragment
android:id="@+id/editorStreamBrowseFragment"
android:name="com.aurora.store.view.ui.commons.EditorStreamBrowseFragment"
tools:layout="@layout/fragment_generic_with_toolbar" >
<argument android:name="title"
app:argType="string" />
<argument
android:name="browseUrl"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/expandedStreamBrowseFragment"
android:name="com.aurora.store.view.ui.commons.ExpandedStreamBrowseFragment"
@@ -273,11 +255,8 @@
android:name="com.aurora.store.view.ui.commons.StreamBrowseFragment"
tools:layout="@layout/fragment_generic_with_toolbar" >
<argument
android:name="browseUrl"
app:argType="string" />
<argument
android:name="title"
app:argType="string" />
android:name="cluster"
app:argType="com.aurora.gplayapi.data.models.StreamCluster" />
</fragment>
<fragment
android:id="@+id/devAppsFragment"

View File

@@ -93,6 +93,7 @@
<string name="action_search">"Search"</string>
<string name="action_share">"Share"</string>
<string name="action_uninstall">"Uninstall"</string>
<string name="action_uninstall_success">"Successfully uninstalled"</string>
<string name="action_home_screen">Add to Home screen</string>
<string name="action_uninstall_confirmation">"Do you want to uninstall this app ?"</string>
<string name="action_info">"App Info"</string>

View File

@@ -7,7 +7,7 @@ composeBom = "2024.06.00"
coreVersion = "1.13.1"
epoxyVersion = "5.1.4"
espressoVersion = "3.6.1"
gplayapiVersion = "3.3.0"
gplayapiVersion = "3.3.1"
gsonVersion = "2.10.1"
hiddenapibypassVersion = "4.3"
hiltVersion = "2.51.1"