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

View File

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

View File

@@ -6,9 +6,9 @@ import kotlinx.coroutines.flow.asSharedFlow
import javax.inject.Singleton import javax.inject.Singleton
@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) private val _busEvent = MutableSharedFlow<BusEvent>(extraBufferCapacity = 1)
val busEvent = _busEvent.asSharedFlow() val busEvent = _busEvent.asSharedFlow()
@@ -16,10 +16,14 @@ class FlowEvent {
private val _installerEvent = MutableSharedFlow<InstallerEvent>(extraBufferCapacity = 1) private val _installerEvent = MutableSharedFlow<InstallerEvent>(extraBufferCapacity = 1)
val installerEvent = _installerEvent.asSharedFlow() 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) { when (event) {
is InstallerEvent -> _installerEvent.tryEmit(event) is InstallerEvent -> _installerEvent.tryEmit(event)
is BusEvent -> _busEvent.tryEmit(event) is BusEvent -> _busEvent.tryEmit(event)
is AuthEvent -> _authEvent.tryEmit(event)
else -> Log.e(TAG, "Got an unhandled 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?) { open fun postError(packageName: String, error: String?, extra: String?) {
Log.e("Service Error :$error") Log.e("Service Error :$error")
val event = InstallerEvent.Failed( val event = InstallerEvent.Failed(packageName).apply {
packageName, this.error = error ?: ""
error, this.extra = 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()) { val downloadDir = if (sharedLibPackageName.isNotBlank()) {
PathUtil.getLibDownloadDir(context, packageName, versionCode, sharedLibPackageName) PathUtil.getLibDownloadDir(context, packageName, versionCode, sharedLibPackageName)
} else { } else {

View File

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

View File

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

View File

@@ -36,7 +36,9 @@ import com.aurora.extensions.isSAndAbove
import com.aurora.extensions.isTAndAbove import com.aurora.extensions.isTAndAbove
import com.aurora.extensions.isUAndAbove import com.aurora.extensions.isUAndAbove
import com.aurora.extensions.runOnUiThread import com.aurora.extensions.runOnUiThread
import com.aurora.store.AuroraApp
import com.aurora.store.R 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.ACTION_INSTALL_STATUS
import com.aurora.store.data.installer.AppInstaller.Companion.EXTRA_DISPLAY_NAME import com.aurora.store.data.installer.AppInstaller.Companion.EXTRA_DISPLAY_NAME
import com.aurora.store.data.installer.AppInstaller.Companion.EXTRA_PACKAGE_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.Log
import com.aurora.store.util.PackageUtil.isSharedLibraryInstalled import com.aurora.store.util.PackageUtil.isSharedLibraryInstalled
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -69,27 +72,38 @@ class SessionInstaller @Inject constructor(
override fun onActiveChanged(sessionId: Int, active: Boolean) {} 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) { override fun onFinished(sessionId: Int, success: Boolean) {
enqueuedSessions.find { set -> set.any { it.sessionId == sessionId } }?.let { sessionSet -> enqueuedSessions.find { set -> set.any { it.sessionId == sessionId } }
sessionSet.remove(sessionSet.first { 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 this was a shared lib, proceed installing other libs or actual package
if (sessionSet.isNotEmpty() && success) { if (sessionSet.isNotEmpty() && success) {
commitInstall(sessionSet.first()); return commitInstall(sessionSet.first()); return
} else { } else {
enqueuedSessions.remove(sessionSet) enqueuedSessions.remove(sessionSet)
}
} }
}
if (enqueuedSessions.isNotEmpty()) { if (enqueuedSessions.isNotEmpty()) {
enqueuedSessions.firstOrNull()?.let { sessionSet -> enqueuedSessions.firstOrNull()?.let { sessionSet ->
commitInstall(sessionSet.first()) 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) { override fun install(download: Download) {
super.install(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) { if (sessionSet != null) {
Log.i("${download.packageName} already queued") Log.i("${download.packageName} already queued")
commitInstall(sessionSet.first()) commitInstall(sessionSet.first())
@@ -124,7 +139,11 @@ class SessionInstaller @Inject constructor(
download.sharedLibs.forEach { download.sharedLibs.forEach {
// Shared library packages cannot be updated // Shared library packages cannot be updated
if (!isSharedLibraryInstalled(context, it.packageName, it.versionCode)) { 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)) 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? { private fun stageInstall(
val packageInstaller = context.packageManager.packageInstaller packageName: String,
versionCode: Int,
sharedLibPkgName: String = ""
): Int? {
val resolvedPackageName = sharedLibPkgName.ifBlank { packageName }
val sessionParams = SessionParams(SessionParams.MODE_FULL_INSTALL).apply { val sessionParams = buildSessionParams(resolvedPackageName)
setAppPackageName(sharedLibPkgName.ifBlank { packageName }) val sessionId = packageInstaller.createSession(sessionParams)
setInstallLocation(PackageInfo.INSTALL_LOCATION_AUTO) val session = packageInstaller.openSession(sessionId)
if (isOAndAbove()) {
setInstallReason(PackageManager.INSTALL_REASON_USER) 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()) { if (isNAndAbove()) {
setOriginatingUid(Process.myUid()) setOriginatingUid(Process.myUid())
} }
if (isOAndAbove()) {
setInstallReason(PackageManager.INSTALL_REASON_USER)
}
if (isSAndAbove()) { if (isSAndAbove()) {
setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED) setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED)
} }
@@ -170,26 +221,6 @@ class SessionInstaller @Inject constructor(
setRequestUpdateOwnership(true) 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) { private fun commitInstall(sessionInfo: SessionInfo) {

View File

@@ -29,5 +29,6 @@ enum class State {
QUEUED, QUEUED,
PROGRESS, PROGRESS,
COMPLETE, 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 = val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification = NotificationUtil.getInstallerStatusNotification( val notification = NotificationUtil.getInstallerStatusNotification(
@@ -109,23 +114,24 @@ class InstallerStatusReceiver : BroadcastReceiver() {
private fun postStatus(status: Int, packageName: String?, extra: String?, context: Context) { private fun postStatus(status: Int, packageName: String?, extra: String?, context: Context) {
val event = when (status) { val event = when (status) {
PackageInstaller.STATUS_SUCCESS -> { PackageInstaller.STATUS_SUCCESS -> {
InstallerEvent.Success( InstallerEvent.Installed(packageName!!).apply {
packageName, context.getString(R.string.installer_status_success) this.extra = context.getString(R.string.installer_status_success)
) }
} }
PackageInstaller.STATUS_FAILURE_ABORTED -> { PackageInstaller.STATUS_FAILURE_ABORTED -> {
InstallerEvent.Cancelled( InstallerEvent.Cancelled(packageName!!).apply {
packageName, AppInstaller.getErrorString(context, status) this.extra = AppInstaller.getErrorString(context, status)
) }
} }
else -> { else -> {
InstallerEvent.Failed( InstallerEvent.Failed(packageName!!).apply {
packageName, AppInstaller.getErrorString(context, status), extra 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.Context
import android.content.Intent import android.content.Intent
import com.aurora.store.AuroraApp import com.aurora.store.AuroraApp
import com.aurora.store.data.event.BusEvent.InstallEvent import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.event.BusEvent.UninstallEvent
import com.aurora.store.data.installer.AppInstaller import com.aurora.store.data.installer.AppInstaller
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
@@ -41,11 +40,11 @@ open class PackageManagerReceiver : BroadcastReceiver() {
when (intent.action) { when (intent.action) {
Intent.ACTION_PACKAGE_ADDED -> { Intent.ACTION_PACKAGE_ADDED -> {
AuroraApp.flowEvent.emitEvent(InstallEvent(packageName, "")) AuroraApp.events.send(InstallerEvent.Installed(packageName))
} }
Intent.ACTION_PACKAGE_REMOVED -> { 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 android.widget.RelativeLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.aurora.extensions.getString import com.aurora.extensions.getString
import com.aurora.extensions.runOnUiThread
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.model.State import com.aurora.store.data.model.State
import com.aurora.store.databinding.ViewActionButtonBinding import com.aurora.store.databinding.ViewActionButtonBinding
@@ -101,19 +100,21 @@ class ActionButton : RelativeLayout {
binding.btn.text = getString(text) binding.btn.text = getString(text)
} }
fun setButtonState(enabled: Boolean = true) {
binding.btn.isEnabled = enabled
}
fun updateState(state: State) { fun updateState(state: State) {
val displayChild = when (state) { val displayChild = when (state) {
State.IDLE -> 0
State.PROGRESS -> 1 State.PROGRESS -> 1
State.COMPLETE -> 2 State.COMPLETE -> 2
else -> 0 else -> 0
} }
if (binding.viewFlipper.displayedChild != displayChild) { if (binding.viewFlipper.displayedChild != displayChild) {
runOnUiThread { binding.viewFlipper.displayedChild = displayChild
binding.viewFlipper.displayedChild = displayChild
if (displayChild == 2) updateState(State.IDLE) if (displayChild == 2) updateState(State.IDLE)
}
} }
} }

View File

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

View File

@@ -19,13 +19,17 @@
package com.aurora.store.view.epoxy.views.app package com.aurora.store.view.epoxy.views.app
import android.animation.ObjectAnimator
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.load import coil.load
import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation import coil.transform.RoundedCornersTransformation
import com.airbnb.epoxy.CallbackProp import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp import com.airbnb.epoxy.ModelProp
@@ -51,6 +55,8 @@ class AppUpdateView @JvmOverloads constructor(
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0 defStyleAttr: Int = 0
) : BaseView<ViewAppUpdateBinding>(context, attrs, defStyleAttr) { ) : BaseView<ViewAppUpdateBinding>(context, attrs, defStyleAttr) {
private var iconDrawable: Drawable? = null
private val cornersTransformation = RoundedCornersTransformation(8.px.toFloat())
@ModelProp @ModelProp
fun app(app: App) { fun app(app: App) {
@@ -59,7 +65,10 @@ class AppUpdateView @JvmOverloads constructor(
binding.txtLine1.text = displayName binding.txtLine1.text = displayName
binding.imgIcon.load(iconArtwork.url) { binding.imgIcon.load(iconArtwork.url) {
placeholder(R.drawable.bg_placeholder) placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(8.px.toFloat())) transformations(cornersTransformation)
listener { _, result ->
result.drawable.let { iconDrawable = it }
}
} }
binding.txtLine2.text = developerName binding.txtLine2.text = developerName
@@ -90,29 +99,22 @@ class AppUpdateView @JvmOverloads constructor(
@ModelProp @ModelProp
fun download(download: Download?) { fun download(download: Download?) {
if (download != null) { if (download != null) {
/*Inflate Download details*/
binding.btnAction.updateState(download.downloadStatus) binding.btnAction.updateState(download.downloadStatus)
binding.progressDownload.isIndeterminate = download.progress < 1
when (download.downloadStatus) { when (download.downloadStatus) {
DownloadStatus.QUEUED -> { DownloadStatus.QUEUED -> {
binding.progressDownload.progress = 0 binding.progressDownload.isIndeterminate = true
binding.progressDownload.show() animateImageView(scaleFactor = 0.75f)
} }
DownloadStatus.DOWNLOADING -> { DownloadStatus.DOWNLOADING -> {
if (download.progress > 0) { binding.progressDownload.isIndeterminate = false
if (download.progress == 100) { binding.progressDownload.progress = download.progress
binding.progressDownload.invisible() animateImageView(scaleFactor = 0.75f)
} else {
binding.progressDownload.progress = download.progress
binding.progressDownload.show()
}
}
} }
else -> { else -> {
binding.progressDownload.progress = 0 binding.progressDownload.isIndeterminate = true
binding.progressDownload.invisible() animateImageView(scaleFactor = 1f)
} }
} }
} }
@@ -141,7 +143,48 @@ class AppUpdateView @JvmOverloads constructor(
@OnViewRecycled @OnViewRecycled
fun clear() { fun clear() {
binding.headerIndicator.removeCallbacks { } binding.headerIndicator.removeCallbacks { }
binding.progressDownload.progress = 0
binding.progressDownload.invisible() 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 androidx.navigation.fragment.findNavController
import com.aurora.store.AuroraApp import com.aurora.store.AuroraApp
import com.aurora.store.R 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.databinding.FragmentGoogleBinding
import com.aurora.store.util.AC2DMUtil import com.aurora.store.util.AC2DMUtil
import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.view.ui.commons.BaseFragment
@@ -110,15 +110,15 @@ class GoogleFragment : BaseFragment<FragmentGoogleBinding>() {
} }
viewLifecycleOwner.lifecycleScope.launch { 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) { when (event) {
is BusEvent.GoogleAAS -> { is AuthEvent.GoogleLogin -> {
if (event.success) { if (event.success) {
viewModel.buildGoogleAuthData(event.email, event.aasToken) viewModel.buildGoogleAuthData(event.email, event.token)
} else { } else {
Toast.makeText( Toast.makeText(
requireContext(), requireContext(),

View File

@@ -65,8 +65,6 @@ class AppsGamesFragment : BaseFragment<FragmentGenericWithPagerBinding>() {
) { tab: TabLayout.Tab, position: Int -> ) { tab: TabLayout.Tab, position: Int ->
when (position) { when (position) {
0 -> tab.text = getString(R.string.title_installed) 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 -> {} else -> {}
} }
}.attach() }.attach()
@@ -81,14 +79,12 @@ class AppsGamesFragment : BaseFragment<FragmentGenericWithPagerBinding>() {
override fun createFragment(position: Int): Fragment { override fun createFragment(position: Int): Fragment {
return when (position) { return when (position) {
0 -> InstalledAppsFragment.newInstance() 0 -> InstalledAppsFragment.newInstance()
1 -> LibraryAppsFragment.newInstance()
2 -> PurchasedAppsFragment.newInstance()
else -> Fragment() else -> Fragment()
} }
} }
override fun getItemCount(): Int { 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.gplayapi.data.models.App
import com.aurora.store.AuroraApp import com.aurora.store.AuroraApp
import com.aurora.store.data.event.BusEvent 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.databinding.FragmentAppsBinding
import com.aurora.store.view.epoxy.views.HeaderViewModel_ 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.AppListViewModel_
@@ -49,10 +51,10 @@ class InstalledAppsFragment : BaseFragment<FragmentAppsBinding>() {
} }
} }
private fun onEvent(event: BusEvent) { private fun onEvent(event: Event) {
when (event) { when (event) {
is BusEvent.InstallEvent, is InstallerEvent.Installed,
is BusEvent.UninstallEvent, is InstallerEvent.Uninstalled,
is BusEvent.Blacklisted -> { is BusEvent.Blacklisted -> {
viewModel.fetchApps() viewModel.fetchApps()
} }
@@ -73,7 +75,7 @@ class InstalledAppsFragment : BaseFragment<FragmentAppsBinding>() {
} }
viewLifecycleOwner.lifecycleScope.launch { 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.util.Preferences
import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.view.ui.commons.CategoryFragment 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.ForYouFragment
import com.aurora.store.view.ui.commons.TopChartContainerFragment import com.aurora.store.view.ui.commons.TopChartContainerFragment
import com.google.android.material.tabs.TabLayout 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_top_charts))
add(getString(R.string.tab_categories)) add(getString(R.string.tab_categories))
if (!authProvider.isAnonymous) {
add(getString(R.string.tab_editor_choice))
}
} }
TabLayoutMediator( TabLayoutMediator(
@@ -130,10 +125,6 @@ class AppsContainerFragment : BaseFragment<FragmentAppsGamesBinding>() {
} }
add(TopChartContainerFragment.newInstance(0)) add(TopChartContainerFragment.newInstance(0))
add(CategoryFragment.newInstance(0)) add(CategoryFragment.newInstance(0))
if (isGoogleAccount) {
add(EditorChoiceFragment.newInstance(0))
}
} }
override fun createFragment(position: Int): Fragment { override fun createFragment(position: Int): Fragment {

View File

@@ -28,6 +28,7 @@ import androidx.navigation.fragment.findNavController
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Category import com.aurora.gplayapi.data.models.Category
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.MobileNavigationDirections import com.aurora.store.MobileNavigationDirections
import com.google.gson.Gson import com.google.gson.Gson
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
@@ -93,19 +94,12 @@ abstract class BaseFragment<ViewBindingType : ViewBinding> : Fragment() {
title title
) )
) )
} else {
findNavController().navigate(
MobileNavigationDirections.actionGlobalStreamBrowseFragment(
browseUrl,
title
)
)
} }
} }
fun openEditorStreamBrowseFragment(browseUrl: String, title: String = "") { fun openStreamBrowseFragment(streamCluster: StreamCluster) {
findNavController().navigate( 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.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.helpers.contracts.StreamContract 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
import com.aurora.store.data.model.ViewState.Loading.getDataAs import com.aurora.store.data.model.ViewState.Loading.getDataAs
import com.aurora.store.databinding.FragmentGenericWithToolbarBinding import com.aurora.store.databinding.FragmentGenericWithToolbarBinding
import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener
import com.aurora.store.view.epoxy.controller.CategoryCarouselController import com.aurora.store.view.epoxy.controller.CategoryCarouselController
import com.aurora.store.view.epoxy.controller.GenericCarouselController 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 import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint @AndroidEntryPoint
class CategoryBrowseFragment : BaseFragment<FragmentGenericWithToolbarBinding>(), class CategoryBrowseFragment : BaseFragment<FragmentGenericWithToolbarBinding>(),
GenericCarouselController.Callbacks { GenericCarouselController.Callbacks {
private val args: CategoryBrowseFragmentArgs by navArgs() 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 lateinit var category: StreamContract.Category
private var streamBundle: StreamBundle? = StreamBundle() private var streamBundle: StreamBundle? = StreamBundle()
@@ -49,8 +50,7 @@ class CategoryBrowseFragment : BaseFragment<FragmentGenericWithToolbarBinding>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val rawCategory = args.browseUrl.split("/").last() category = CategoryUtil.getCategoryFromUrl(args.browseUrl)
category = StreamContract.Category.NONE.apply { value = rawCategory }
val genericCarouselController = CategoryCarouselController(this) val genericCarouselController = CategoryCarouselController(this)
@@ -68,6 +68,7 @@ class CategoryBrowseFragment : BaseFragment<FragmentGenericWithToolbarBinding>()
} }
}) })
viewModel.getStreamBundle(category)
viewModel.liveData.observe(viewLifecycleOwner) { viewModel.liveData.observe(viewLifecycleOwner) {
when (it) { when (it) {
is ViewState.Loading -> { is ViewState.Loading -> {
@@ -84,12 +85,10 @@ class CategoryBrowseFragment : BaseFragment<FragmentGenericWithToolbarBinding>()
else -> {} else -> {}
} }
} }
viewModel.observe(category)
} }
override fun onHeaderClicked(streamCluster: StreamCluster) { override fun onHeaderClicked(streamCluster: StreamCluster) {
openStreamBrowseFragment(streamCluster)
} }
override fun onClusterScrolled(streamCluster: 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) pageType = bundle.getInt(Constants.PAGE_TYPE, 0)
} }
when (pageType) { category = if (pageType == 0) Category.APPLICATION else Category.GAME
0 -> {
category = Category.APPLICATION
viewModel.getStreamBundle(category, Type.HOME)
}
1 -> {
category = Category.GAME
viewModel.getStreamBundle(category, Type.HOME)
}
}
binding.recycler.setController(genericCarouselController) binding.recycler.setController(genericCarouselController)
binding.recycler.addOnScrollListener( binding.recycler.addOnScrollListener(
object : EndlessRecyclerOnScrollListener() { object : EndlessRecyclerOnScrollListener(visibleThreshold = 4) {
override fun onLoadMore(currentPage: Int) { override fun onLoadMore(currentPage: Int) {
viewModel.observe(category, Type.HOME) viewModel.observe(category, Type.HOME)
} }
} }
) )
viewModel.getStreamBundle(category, Type.HOME)
viewModel.liveData.observe(viewLifecycleOwner) { viewModel.liveData.observe(viewLifecycleOwner) {
when (it) { when (it) {
is ViewState.Loading -> { is ViewState.Loading -> {
@@ -107,7 +98,7 @@ class ForYouFragment : BaseFragment<FragmentForYouBinding>(),
} }
override fun onHeaderClicked(streamCluster: StreamCluster) { override fun onHeaderClicked(streamCluster: StreamCluster) {
openStreamBrowseFragment(streamCluster)
} }
override fun onClusterScrolled(streamCluster: StreamCluster) { override fun onClusterScrolled(streamCluster: StreamCluster) {

View File

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

View File

@@ -38,38 +38,32 @@ class StreamBrowseFragment : BaseFragment<FragmentGenericWithToolbarBinding>() {
private val args: StreamBrowseFragmentArgs by navArgs() private val args: StreamBrowseFragmentArgs by navArgs()
private val viewModel: StreamBrowseViewModel by viewModels() private val viewModel: StreamBrowseViewModel by viewModels()
private lateinit var endlessRecyclerOnScrollListener: EndlessRecyclerOnScrollListener private lateinit var streamCluster: StreamCluster
private lateinit var cluster: StreamCluster
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
streamCluster = args.cluster
// Toolbar // Toolbar
binding.layoutToolbarAction.apply { binding.layoutToolbarAction.apply {
txtTitle.text = args.title txtTitle.text = streamCluster.clusterTitle
imgActionPrimary.setOnClickListener { imgActionPrimary.setOnClickListener {
findNavController().navigateUp() findNavController().navigateUp()
} }
} }
viewModel.liveData.observe(viewLifecycleOwner) { binding.recycler.addOnScrollListener(object :
if (!::cluster.isInitialized) { EndlessRecyclerOnScrollListener(visibleThreshold = 4) {
endlessRecyclerOnScrollListener = object : EndlessRecyclerOnScrollListener() { override fun onLoadMore(currentPage: Int) {
override fun onLoadMore(currentPage: Int) { viewModel.nextCluster()
viewModel.nextCluster()
}
}
binding.recycler.addOnScrollListener(endlessRecyclerOnScrollListener)
} }
})
cluster = it viewModel.initCluster(streamCluster)
viewModel.liveData.observe(viewLifecycleOwner) {
updateController(cluster) updateController(streamCluster)
binding.layoutToolbarAction.txtTitle.text = it.clusterTitle
} }
viewModel.getStreamBundle(args.browseUrl)
updateController(null)
} }
private fun updateController(streamCluster: StreamCluster?) { 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.Review
import com.aurora.gplayapi.data.models.StreamBundle import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.AuroraApp
import com.aurora.store.AppStreamStash import com.aurora.store.AppStreamStash
import com.aurora.store.AuroraApp
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.event.BusEvent import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.event.Event import com.aurora.store.data.event.Event
@@ -135,7 +135,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
private fun onEvent(event: Event) { private fun onEvent(event: Event) {
when (event) { when (event) {
is BusEvent.InstallEvent -> { is InstallerEvent.Installed -> {
if (app.packageName == event.packageName) { if (app.packageName == event.packageName) {
attachActions() attachActions()
binding.layoutDetailsToolbar.toolbar.menu.apply { binding.layoutDetailsToolbar.toolbar.menu.apply {
@@ -147,7 +147,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
} }
is BusEvent.UninstallEvent -> { is InstallerEvent.Uninstalled -> {
if (app.packageName == event.packageName) { if (app.packageName == event.packageName) {
attachActions() attachActions()
binding.layoutDetailsToolbar.toolbar.menu.apply { binding.layoutDetailsToolbar.toolbar.menu.apply {
@@ -166,14 +166,23 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
is InstallerEvent.Failed -> { is InstallerEvent.Failed -> {
findNavController().navigate( if (app.packageName == event.packageName) {
AppDetailsFragmentDirections.actionAppDetailsFragmentToInstallErrorDialogSheet( findNavController().navigate(
app, AppDetailsFragmentDirections.actionAppDetailsFragmentToInstallErrorDialogSheet(
event.packageName ?: "", app,
event.error ?: "", event.packageName ?: "",
event.extra ?: "" event.error ?: "",
event.extra ?: ""
)
) )
) }
}
is InstallerEvent.Installing -> {
if (event.packageName == app.packageName) {
attachActions()
updateActionState(State.INSTALLING)
}
} }
else -> { else -> {
@@ -356,10 +365,10 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
AuroraApp.flowEvent.busEvent.collect { onEvent(it) } AuroraApp.events.busEvent.collect { onEvent(it) }
} }
viewLifecycleOwner.lifecycleScope.launch { 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) { 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() { private fun openApp() {
@@ -589,9 +606,8 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName) app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName)
runOnUiThread { runOnUiThread {
app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName)
binding.layoutDetailsInstall.btnDownload.let { btn -> binding.layoutDetailsInstall.btnDownload.let { btn ->
btn.setButtonState(true)
if (app.isInstalled) { if (app.isInstalled) {
isUpdatable = PackageUtil.isUpdatable( isUpdatable = PackageUtil.isUpdatable(
requireContext(), requireContext(),

View File

@@ -33,7 +33,6 @@ import com.aurora.store.databinding.FragmentAppsGamesBinding
import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences
import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.view.ui.commons.CategoryFragment 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.ForYouFragment
import com.aurora.store.view.ui.commons.TopChartContainerFragment import com.aurora.store.view.ui.commons.TopChartContainerFragment
import com.google.android.material.tabs.TabLayout 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_top_charts))
add(getString(R.string.tab_categories)) add(getString(R.string.tab_categories))
if (!authProvider.isAnonymous) {
add(getString(R.string.tab_editor_choice))
}
} }
TabLayoutMediator( TabLayoutMediator(
@@ -129,10 +124,6 @@ class GamesContainerFragment : BaseFragment<FragmentAppsGamesBinding>() {
add(TopChartContainerFragment.newInstance(1)) add(TopChartContainerFragment.newInstance(1))
add(CategoryFragment.newInstance(1)) add(CategoryFragment.newInstance(1))
if (isGoogleAccount) {
add(EditorChoiceFragment.newInstance(1))
}
} }
override fun createFragment(position: Int): Fragment { 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() dismissAllowingStateLoss()
AuroraApp.flowEvent.emitEvent( AuroraApp.events.send(
BusEvent.Blacklisted(args.app.packageName, "") 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.AuroraApp
import com.aurora.store.MobileNavigationDirections import com.aurora.store.MobileNavigationDirections
import com.aurora.store.R 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.data.room.download.Download
import com.aurora.store.databinding.FragmentUpdatesBinding import com.aurora.store.databinding.FragmentUpdatesBinding
import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil
@@ -143,13 +144,13 @@ class UpdatesFragment : BaseFragment<FragmentUpdatesBinding>() {
} }
viewLifecycleOwner.lifecycleScope.launch { 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) { when (event) {
is BusEvent.InstallEvent, is BusEvent.UninstallEvent -> { is InstallerEvent.Installed, is InstallerEvent.Uninstalled -> {
viewModel.fetchUpdates() 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.gplayapi.helpers.AuthHelper
import com.aurora.store.AuroraApp import com.aurora.store.AuroraApp
import com.aurora.store.R 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.AccountType
import com.aurora.store.data.model.AuthState import com.aurora.store.data.model.AuthState
import com.aurora.store.data.model.InsecureAuth import com.aurora.store.data.model.InsecureAuth
@@ -201,18 +201,18 @@ class AuthViewModel @Inject constructor(
if (aasToken != null) { if (aasToken != null) {
Preferences.putString(context, Constants.ACCOUNT_EMAIL_PLAIN, email) Preferences.putString(context, Constants.ACCOUNT_EMAIL_PLAIN, email)
Preferences.putString(context, Constants.ACCOUNT_AAS_PLAIN, aasToken) 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 { } else {
Preferences.putString(context, Constants.ACCOUNT_EMAIL_PLAIN, "") Preferences.putString(context, Constants.ACCOUNT_EMAIL_PLAIN, "")
Preferences.putString(context, Constants.ACCOUNT_AAS_PLAIN, "") Preferences.putString(context, Constants.ACCOUNT_AAS_PLAIN, "")
AuroraApp.flowEvent.emitEvent(BusEvent.GoogleAAS(false)) AuroraApp.events.send(AuthEvent.GoogleLogin(false, "", ""))
} }
} else { } else {
AuroraApp.flowEvent.emitEvent(BusEvent.GoogleAAS(false)) AuroraApp.events.send(AuthEvent.GoogleLogin(false, "", ""))
} }
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e(TAG, "Failed to build AuthData", 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) buildGoogleAuthData(email, aasToken)
} }
AccountType.ANONYMOUS -> { AccountType.ANONYMOUS -> {
buildAnonymousAuthData() buildAnonymousAuthData()
} }
@@ -248,9 +249,11 @@ class AuthViewModel @Inject constructor(
is UnknownHostException -> { is UnknownHostException -> {
context.getString(R.string.title_no_network) context.getString(R.string.title_no_network)
} }
is ConnectException -> { is ConnectException -> {
context.getString(R.string.server_unreachable) context.getString(R.string.server_unreachable)
} }
else -> { else -> {
context.getString(R.string.bad_request) context.getString(R.string.bad_request)
} }

View File

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

View File

@@ -28,7 +28,6 @@ import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.Category import com.aurora.gplayapi.data.models.Category
import com.aurora.gplayapi.helpers.CategoryHelper import com.aurora.gplayapi.helpers.CategoryHelper
import com.aurora.gplayapi.helpers.contracts.CategoryContract import com.aurora.gplayapi.helpers.contracts.CategoryContract
import com.aurora.gplayapi.helpers.web.WebCategoryHelper
import com.aurora.store.CategoryStash import com.aurora.store.CategoryStash
import com.aurora.store.data.model.ViewState import com.aurora.store.data.model.ViewState
import com.aurora.store.data.network.HttpClient import com.aurora.store.data.network.HttpClient
@@ -50,9 +49,6 @@ class CategoryViewModel @Inject constructor(
private val categoryHelper: CategoryHelper = CategoryHelper(authProvider.authData) private val categoryHelper: CategoryHelper = CategoryHelper(authProvider.authData)
.using(HttpClient.getPreferredClient(context)) .using(HttpClient.getPreferredClient(context))
private val webCategoryHelper: CategoryContract = WebCategoryHelper()
.using(HttpClient.getPreferredClient(context))
private var stash: CategoryStash = mutableMapOf( private var stash: CategoryStash = mutableMapOf(
Category.Type.APPLICATION to emptyList(), Category.Type.APPLICATION to emptyList(),
Category.Type.GAME to emptyList() Category.Type.GAME to emptyList()
@@ -61,11 +57,7 @@ class CategoryViewModel @Inject constructor(
val liveData = MutableLiveData<ViewState>() val liveData = MutableLiveData<ViewState>()
private fun contract(): CategoryContract { private fun contract(): CategoryContract {
return if (authProvider.isAnonymous) { return categoryHelper
webCategoryHelper
} else {
categoryHelper
}
} }
fun getCategoryList(type: Category.Type) { fun getCategoryList(type: Category.Type) {
@@ -74,10 +66,11 @@ class CategoryViewModel @Inject constructor(
if (categories.isNotEmpty()) { if (categories.isNotEmpty()) {
liveData.postValue(ViewState.Success(stash)) liveData.postValue(ViewState.Success(stash))
return@launch
} }
try { try {
stash[type] = contract().getAllCategoriesList(type) stash[type] = contract().getAllCategories(type)
liveData.postValue(ViewState.Success(stash)) liveData.postValue(ViewState.Success(stash))
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e(TAG, "Failed fetching list of categories", 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 androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.StreamBundle import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster 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.contracts.StreamContract
import com.aurora.gplayapi.helpers.web.WebStreamHelper import com.aurora.gplayapi.helpers.web.WebStreamHelper
import com.aurora.store.HomeStash import com.aurora.store.HomeStash
import com.aurora.store.data.model.ViewState import com.aurora.store.data.model.ViewState
import com.aurora.store.data.network.HttpClient import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.util.Log import com.aurora.store.util.Log
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
@@ -44,14 +42,10 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
@SuppressLint("StaticFieldLeak") // false positive, see https://github.com/google/dagger/issues/3253 @SuppressLint("StaticFieldLeak") // false positive, see https://github.com/google/dagger/issues/3253
class StreamViewModel @Inject constructor( class StreamViewModel @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context
private val authProvider: AuthProvider
) : ViewModel() { ) : ViewModel() {
private var streamHelper: StreamContract = StreamHelper(authProvider.authData) private var webStreamHelper = WebStreamHelper()
.using(HttpClient.getPreferredClient(context))
private var webStreamHelper: StreamContract = WebStreamHelper()
.using(HttpClient.getPreferredClient(context)) .using(HttpClient.getPreferredClient(context))
val liveData: MutableLiveData<ViewState> = MutableLiveData() val liveData: MutableLiveData<ViewState> = MutableLiveData()
@@ -59,11 +53,7 @@ class StreamViewModel @Inject constructor(
private var stash: HomeStash = mutableMapOf() private var stash: HomeStash = mutableMapOf()
fun contract(): StreamContract { fun contract(): StreamContract {
return if (authProvider.isAnonymous) { return webStreamHelper
webStreamHelper
} else {
streamHelper
}
} }
fun getStreamBundle(category: StreamContract.Category, type: StreamContract.Type) { fun getStreamBundle(category: StreamContract.Category, type: StreamContract.Type) {
@@ -75,7 +65,7 @@ class StreamViewModel @Inject constructor(
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
supervisorScope { supervisorScope {
val bundle = targetBundle(category) val bundle = targetBundle(category)
if (bundle.streamClusters.isNotEmpty()) { if (bundle.hasCluster()) {
liveData.postValue(ViewState.Success(stash)) liveData.postValue(ViewState.Success(stash))
} }
@@ -83,13 +73,13 @@ class StreamViewModel @Inject constructor(
if (!bundle.hasCluster() || bundle.hasNext()) { if (!bundle.hasCluster() || bundle.hasNext()) {
//Fetch new stream bundle //Fetch new stream bundle
val newBundle = if (bundle.streamClusters.isEmpty()) { val newBundle = if (bundle.hasCluster()) {
contract().fetch(type, category)
} else {
contract().nextStreamBundle( contract().nextStreamBundle(
category, category,
bundle.streamNextPageUrl bundle.streamNextPageUrl
) )
} else {
contract().fetch(type, category)
} }
//Update old bundle //Update old bundle
@@ -142,7 +132,7 @@ class StreamViewModel @Inject constructor(
} }
private fun targetBundle(category: StreamContract.Category): StreamBundle { private fun targetBundle(category: StreamContract.Category): StreamBundle {
val streamBundle = stash.getOrPut(category){ val streamBundle = stash.getOrPut(category) {
StreamBundle() 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 purchaseHelper = PurchaseHelper(authProvider.authData)
val files = purchaseHelper.purchase(app.packageName, customVersion, app.offerType) val files = purchaseHelper.purchase(app.packageName, customVersion, app.offerType)
if (files.isNotEmpty()) { if (files.isNotEmpty()) {
AuroraApp.flowEvent.emitEvent( AuroraApp.events.send(
BusEvent.ManualDownload(app.packageName, customVersion) BusEvent.ManualDownload(app.packageName, customVersion)
) )
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -93,6 +93,7 @@
<string name="action_search">"Search"</string> <string name="action_search">"Search"</string>
<string name="action_share">"Share"</string> <string name="action_share">"Share"</string>
<string name="action_uninstall">"Uninstall"</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_home_screen">Add to Home screen</string>
<string name="action_uninstall_confirmation">"Do you want to uninstall this app ?"</string> <string name="action_uninstall_confirmation">"Do you want to uninstall this app ?"</string>
<string name="action_info">"App Info"</string> <string name="action_info">"App Info"</string>

View File

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