libs: Bump gplyapi to 3.5.0

* Map versionCode as Long
* Handle all cases of immutable object modification

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2025-05-13 15:14:54 +08:00
parent a2a9dbfdfe
commit e39bc3b8a5
36 changed files with 182 additions and 194 deletions

View File

@@ -19,18 +19,8 @@
package com.aurora.extensions package com.aurora.extensions
import com.aurora.gplayapi.data.models.File import com.aurora.gplayapi.data.models.PlayFile
fun <T> MutableList<T>.flushAndAdd(list: List<T>) { fun List<PlayFile>.requiresObbDir(): Boolean {
clear() return this.any { it.type == PlayFile.Type.OBB }
addAll(list)
}
fun <T> MutableSet<T>.flushAndAdd(list: Set<T>) {
clear()
addAll(list)
}
fun List<File>.requiresObbDir(): Boolean {
return this.any { it.type == File.FileType.OBB || it.type == File.FileType.PATCH }
} }

View File

@@ -26,7 +26,7 @@ sealed class BusEvent : Event() {
lateinit var error: String lateinit var error: String
data class Blacklisted(val packageName: String) : BusEvent() data class Blacklisted(val packageName: String) : BusEvent()
data class ManualDownload(val packageName: String, val versionCode: Int) : BusEvent() data class ManualDownload(val packageName: String, val versionCode: Long) : BusEvent()
} }
sealed class AuthEvent : Event() { sealed class AuthEvent : Event() {

View File

@@ -107,7 +107,7 @@ class DownloadHelper @Inject constructor(
* @param packageName Name of the package of the app * @param packageName Name of the package of the app
* @param versionCode Version of the package * @param versionCode Version of the package
*/ */
suspend fun clearDownload(packageName: String, versionCode: Int) { suspend fun clearDownload(packageName: String, versionCode: Long) {
Log.i(TAG, "Clearing downloads for $packageName ($versionCode)") Log.i(TAG, "Clearing downloads for $packageName ($versionCode)")
downloadDao.delete(packageName) downloadDao.delete(packageName)
PathUtil.getAppDownloadDir(context, packageName, versionCode) PathUtil.getAppDownloadDir(context, packageName, versionCode)

View File

@@ -81,7 +81,7 @@ class RootInstaller @Inject constructor(
} }
} }
private fun xInstall(packageName: String, versionCode: Int, sharedLibPkgName: String = "") { private fun xInstall(packageName: String, versionCode: Long, sharedLibPkgName: String = "") {
var totalSize = 0 var totalSize = 0
for (file in getFiles(packageName, versionCode, sharedLibPkgName)) for (file in getFiles(packageName, versionCode, sharedLibPkgName))

View File

@@ -183,7 +183,7 @@ class SessionInstaller @Inject constructor(
private fun stageInstall( private fun stageInstall(
packageName: String, packageName: String,
versionCode: Int, versionCode: Long,
sharedLibPkgName: String = "" sharedLibPkgName: String = ""
): Int? { ): Int? {
val resolvedPackageName = sharedLibPkgName.ifBlank { packageName } val resolvedPackageName = sharedLibPkgName.ifBlank { packageName }

View File

@@ -129,7 +129,7 @@ class ShizukuInstaller @Inject constructor(
private fun install( private fun install(
packageName: String, packageName: String,
versionCode: Int, versionCode: Long,
sharedLibPkgName: String = "", sharedLibPkgName: String = "",
displayName: String = "" displayName: String = ""
) { ) {

View File

@@ -103,7 +103,7 @@ abstract class InstallerBase(private val context: Context) : IInstaller {
fun getFiles( fun getFiles(
packageName: String, packageName: String,
versionCode: Int, versionCode: Long,
sharedLibPackageName: String = "" sharedLibPackageName: String = ""
): List<File> { ): List<File> {
val downloadDir = if (sharedLibPackageName.isNotBlank()) { val downloadDir = if (sharedLibPackageName.isNotBlank()) {

View File

@@ -1,13 +1,9 @@
package com.aurora.store.data.model package com.aurora.store.data.model
import android.content.Context
import android.content.pm.PackageInfo
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Parcelable import android.os.Parcelable
import androidx.core.content.pm.PackageInfoCompat
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.store.data.room.update.Update import com.aurora.store.data.room.update.Update
import com.aurora.store.util.PackageUtil
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@@ -15,7 +11,7 @@ import kotlinx.parcelize.Parcelize
data class MinimalApp( data class MinimalApp(
val packageName: String, val packageName: String,
val versionName: String, val versionName: String,
val versionCode: Int, val versionCode: Long,
val displayName: String, val displayName: String,
@IgnoredOnParcel @IgnoredOnParcel
val icon: Bitmap? = null val icon: Bitmap? = null
@@ -32,14 +28,6 @@ data class MinimalApp(
) )
} }
fun toApp(minimalApp: MinimalApp): App {
return App(minimalApp.packageName).apply {
versionName = minimalApp.versionName ?: ""
versionCode = minimalApp.versionCode
displayName = minimalApp.displayName
}
}
fun fromUpdate(update: Update): MinimalApp { fun fromUpdate(update: Update): MinimalApp {
return MinimalApp( return MinimalApp(
update.packageName, update.packageName,
@@ -49,14 +37,5 @@ data class MinimalApp(
) )
} }
fun fromPackageInfo(context: Context, packageInfo: PackageInfo): MinimalApp {
return MinimalApp(
packageInfo.packageName,
packageInfo.versionName ?: "",
PackageInfoCompat.getLongVersionCode(packageInfo).toInt(),
packageInfo.applicationInfo!!.loadLabel(context.packageManager).toString(),
PackageUtil.getIconForPackage(context, packageInfo.packageName)
)
}
} }
} }

View File

@@ -23,7 +23,7 @@ import android.content.Context
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Artwork import com.aurora.gplayapi.data.models.Artwork
import com.aurora.gplayapi.data.models.EncodedCertificateSet import com.aurora.gplayapi.data.models.EncodedCertificateSet
import com.aurora.gplayapi.data.models.File import com.aurora.gplayapi.data.models.PlayFile
import com.aurora.store.BuildConfig import com.aurora.store.BuildConfig
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.util.CertUtil import com.aurora.store.util.CertUtil
@@ -31,7 +31,7 @@ import com.google.gson.annotations.SerializedName
data class SelfUpdate( data class SelfUpdate(
@SerializedName("version_name") var versionName: String = String(), @SerializedName("version_name") var versionName: String = String(),
@SerializedName("version_code") var versionCode: Int = 0, @SerializedName("version_code") var versionCode: Long = 0,
@SerializedName("aurora_build") var auroraBuild: String = String(), @SerializedName("aurora_build") var auroraBuild: String = String(),
@SerializedName("fdroid_build") var fdroidBuild: String = String(), @SerializedName("fdroid_build") var fdroidBuild: String = String(),
@SerializedName("updated_on") var updatedOn: String = String(), @SerializedName("updated_on") var updatedOn: String = String(),
@@ -63,7 +63,7 @@ data class SelfUpdate(
developerName = "Rahul Kumar Patel", developerName = "Rahul Kumar Patel",
iconArtwork = Artwork(url = "$BASE_URL/$icon"), iconArtwork = Artwork(url = "$BASE_URL/$icon"),
fileList = mutableListOf( fileList = mutableListOf(
File( PlayFile(
name = "${context.packageName}.apk", name = "${context.packageName}.apk",
url = downloadURL, url = downloadURL,
size = selfUpdate.size size = selfUpdate.size

View File

@@ -3,6 +3,6 @@ package com.aurora.store.data.model
data class SessionInfo( data class SessionInfo(
val sessionId: Int, val sessionId: Int,
val packageName: String, val packageName: String,
val versionCode: Int, val versionCode: Long,
val displayName: String = String() val displayName: String = String()
) )

View File

@@ -52,7 +52,7 @@ class InstallerStatusReceiver : BroadcastReceiver() {
if (context != null && intent?.action == ACTION_INSTALL_STATUS) { if (context != null && intent?.action == ACTION_INSTALL_STATUS) {
val packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME)!! val packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME)!!
val displayName = intent.getStringExtra(EXTRA_DISPLAY_NAME)!! val displayName = intent.getStringExtra(EXTRA_DISPLAY_NAME)!!
val versionCode = intent.getIntExtra(EXTRA_VERSION_CODE, -1) val versionCode = intent.getLongExtra(EXTRA_VERSION_CODE, -1)
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1) val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)
val extra = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) val extra = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)

View File

@@ -4,7 +4,7 @@ import android.os.Parcelable
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.File import com.aurora.gplayapi.data.models.PlayFile
import com.aurora.store.data.model.DownloadStatus import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.room.update.Update import com.aurora.store.data.room.update.Update
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@@ -14,7 +14,7 @@ import java.util.Date
@Entity(tableName = "download") @Entity(tableName = "download")
data class Download( data class Download(
@PrimaryKey val packageName: String, @PrimaryKey val packageName: String,
val versionCode: Int, val versionCode: Long,
val offerType: Int, val offerType: Int,
val isInstalled: Boolean, val isInstalled: Boolean,
val displayName: String, val displayName: String,
@@ -27,7 +27,7 @@ data class Download(
var timeRemaining: Long, var timeRemaining: Long,
var totalFiles: Int, var totalFiles: Int,
var downloadedFiles: Int, var downloadedFiles: Int,
var fileList: List<File>, var fileList: List<PlayFile>,
val sharedLibs: List<SharedLib>, val sharedLibs: List<SharedLib>,
val targetSdk: Int = 1, val targetSdk: Int = 1,
val downloadedAt: Long = 0 val downloadedAt: Long = 0

View File

@@ -2,7 +2,7 @@ package com.aurora.store.data.room.download
import androidx.room.ProvidedTypeConverter import androidx.room.ProvidedTypeConverter
import androidx.room.TypeConverter import androidx.room.TypeConverter
import com.aurora.gplayapi.data.models.File import com.aurora.gplayapi.data.models.PlayFile
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import javax.inject.Inject import javax.inject.Inject
@@ -24,13 +24,13 @@ class DownloadConverter @Inject constructor(private val gson: Gson) {
} }
@TypeConverter @TypeConverter
fun toGPlayFileList(string: String): List<File> { fun toGPlayFileList(string: String): List<PlayFile> {
val listType = object : TypeToken<List<File>>() {}.type val listType = object : TypeToken<List<PlayFile>>() {}.type
return gson.fromJson(string, listType) return gson.fromJson(string, listType)
} }
@TypeConverter @TypeConverter
fun fromGPlayFileList(list: List<File>): String { fun fromGPlayFileList(list: List<PlayFile>): String {
return gson.toJson(list) return gson.toJson(list)
} }
} }

View File

@@ -4,7 +4,7 @@ import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import com.aurora.gplayapi.data.models.File import com.aurora.gplayapi.data.models.PlayFile
import com.aurora.store.data.model.DownloadStatus import com.aurora.store.data.model.DownloadStatus
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -18,7 +18,7 @@ interface DownloadDao {
suspend fun updateStatus(packageName: String, downloadStatus: DownloadStatus) suspend fun updateStatus(packageName: String, downloadStatus: DownloadStatus)
@Query("UPDATE download SET fileList=:fileList WHERE packageName=:packageName") @Query("UPDATE download SET fileList=:fileList WHERE packageName=:packageName")
suspend fun updateFiles(packageName: String, fileList: List<File>) suspend fun updateFiles(packageName: String, fileList: List<PlayFile>)
@Query("UPDATE download SET sharedLibs=:sharedLibs WHERE packageName=:packageName") @Query("UPDATE download SET sharedLibs=:sharedLibs WHERE packageName=:packageName")
suspend fun updateSharedLibs(packageName: String, sharedLibs: List<SharedLib>) suspend fun updateSharedLibs(packageName: String, sharedLibs: List<SharedLib>)

View File

@@ -2,14 +2,14 @@ package com.aurora.store.data.room.download
import android.os.Parcelable import android.os.Parcelable
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.File import com.aurora.gplayapi.data.models.PlayFile
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class SharedLib( data class SharedLib(
val packageName: String, val packageName: String,
val versionCode: Int, val versionCode: Long,
var fileList: List<File> var fileList: List<PlayFile>
) : Parcelable { ) : Parcelable {
companion object { companion object {
fun fromApp(app: App): SharedLib { fun fromApp(app: App): SharedLib {

View File

@@ -5,7 +5,7 @@ import android.os.Parcelable
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.File import com.aurora.gplayapi.data.models.PlayFile
import com.aurora.store.data.room.download.SharedLib import com.aurora.store.data.room.download.SharedLib
import com.aurora.store.util.CertUtil import com.aurora.store.util.CertUtil
import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil
@@ -16,7 +16,7 @@ import kotlinx.parcelize.Parcelize
data class Update( data class Update(
@PrimaryKey @PrimaryKey
val packageName: String, val packageName: String,
val versionCode: Int, val versionCode: Long,
val versionName: String, val versionName: String,
val displayName: String, val displayName: String,
val iconURL: String, val iconURL: String,
@@ -27,7 +27,7 @@ data class Update(
val updatedOn: String, val updatedOn: String,
val hasValidCert: Boolean, val hasValidCert: Boolean,
val offerType: Int, val offerType: Int,
var fileList: List<File>, var fileList: List<PlayFile>,
val sharedLibs: List<SharedLib>, val sharedLibs: List<SharedLib>,
val targetSdk: Int = 1 val targetSdk: Int = 1
) : Parcelable { ) : Parcelable {

View File

@@ -24,6 +24,7 @@ import com.aurora.extensions.isPAndAbove
import com.aurora.extensions.isQAndAbove import com.aurora.extensions.isQAndAbove
import com.aurora.extensions.isSAndAbove import com.aurora.extensions.isSAndAbove
import com.aurora.extensions.requiresObbDir import com.aurora.extensions.requiresObbDir
import com.aurora.gplayapi.data.models.PlayFile
import com.aurora.gplayapi.helpers.PurchaseHelper import com.aurora.gplayapi.helpers.PurchaseHelper
import com.aurora.gplayapi.network.IHttpClient import com.aurora.gplayapi.network.IHttpClient
import com.aurora.store.AuroraApp import com.aurora.store.AuroraApp
@@ -55,7 +56,6 @@ import java.security.DigestInputStream
import java.security.MessageDigest import java.security.MessageDigest
import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException
import kotlin.properties.Delegates import kotlin.properties.Delegates
import com.aurora.gplayapi.data.models.File as GPlayFile
/** /**
* An expedited long-running worker to download and trigger installation for given apps. * An expedited long-running worker to download and trigger installation for given apps.
@@ -127,7 +127,7 @@ class DownloadWorker @AssistedInject constructor(
PathUtil.getObbDownloadDir(download.packageName).mkdirs() PathUtil.getObbDownloadDir(download.packageName).mkdirs()
} }
val files = mutableListOf<GPlayFile>() val files = mutableListOf<PlayFile>()
// Check if shared libs are present, if yes, handle them first // Check if shared libs are present, if yes, handle them first
if (download.sharedLibs.isNotEmpty()) { if (download.sharedLibs.isNotEmpty()) {
@@ -245,7 +245,7 @@ class DownloadWorker @AssistedInject constructor(
* @param offerType Offer type of the app (free/paid) * @param offerType Offer type of the app (free/paid)
* @return A list of purchased files * @return A list of purchased files
*/ */
private fun purchase(packageName: String, versionCode: Int, offerType: Int): List<GPlayFile> { private fun purchase(packageName: String, versionCode: Long, offerType: Int): List<PlayFile> {
try { try {
// Android 9.0+ supports key rotation, so purchase with latest certificate's hash // Android 9.0+ supports key rotation, so purchase with latest certificate's hash
return if (isPAndAbove && download.isInstalled) { return if (isPAndAbove && download.isInstalled) {
@@ -267,10 +267,10 @@ class DownloadWorker @AssistedInject constructor(
/** /**
* Downloads the file from the given request. * Downloads the file from the given request.
* Failed downloads aren't removed and persists as long as [CacheWorker] doesn't cleans them. * Failed downloads aren't removed and persists as long as [CacheWorker] doesn't cleans them.
* @param gFile A [GPlayFile] to download * @param gFile A [PlayFile] to download
* @return A [Boolean] indicating whether the file was downloaded or not. * @return A [Boolean] indicating whether the file was downloaded or not.
*/ */
private suspend fun downloadFile(packageName: String, gFile: GPlayFile): Boolean { private suspend fun downloadFile(packageName: String, gFile: PlayFile): Boolean {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
Log.i(TAG, "Downloading $packageName @ ${gFile.name}") Log.i(TAG, "Downloading $packageName @ ${gFile.name}")
val file = PathUtil.getLocalFile(context, gFile, download) val file = PathUtil.getLocalFile(context, gFile, download)
@@ -414,11 +414,11 @@ class DownloadWorker @AssistedInject constructor(
} }
/** /**
* Verifies integrity of a downloaded [GPlayFile]. * Verifies integrity of a downloaded [PlayFile].
* @param gFile [GPlayFile] to verify * @param gFile [PlayFile] to verify
*/ */
@OptIn(ExperimentalStdlibApi::class) @OptIn(ExperimentalStdlibApi::class)
private suspend fun verifyFile(gFile: GPlayFile): Boolean { private suspend fun verifyFile(gFile: PlayFile): Boolean {
val file = PathUtil.getLocalFile(context, gFile, download) val file = PathUtil.getLocalFile(context, gFile, download)
Log.i(TAG, "Verifying $file") Log.i(TAG, "Verifying $file")
@@ -446,7 +446,7 @@ class DownloadWorker @AssistedInject constructor(
} }
} }
private fun deleteFile(file: GPlayFile) { private fun deleteFile(file: PlayFile) {
val apkFile = PathUtil.getLocalFile(context, file, download) val apkFile = PathUtil.getLocalFile(context, file, download)
if (apkFile.exists()) { if (apkFile.exists()) {
apkFile.delete() apkFile.delete()

View File

@@ -77,7 +77,7 @@ class ExportWorker @AssistedInject constructor(
.putString(URI, uri.toString()) .putString(URI, uri.toString())
.putString(DISPLAY_NAME, download.displayName) .putString(DISPLAY_NAME, download.displayName)
.putString(PACKAGE_NAME, download.packageName) .putString(PACKAGE_NAME, download.packageName)
.putInt(VERSION_CODE, download.versionCode) .putLong(VERSION_CODE, download.versionCode)
.build() .build()
val oneTimeWorkRequest = OneTimeWorkRequestBuilder<ExportWorker>() val oneTimeWorkRequest = OneTimeWorkRequestBuilder<ExportWorker>()
@@ -99,11 +99,11 @@ class ExportWorker @AssistedInject constructor(
val uri = inputData.getString(URI)!!.toUri() val uri = inputData.getString(URI)!!.toUri()
val packageName = inputData.getString(PACKAGE_NAME) val packageName = inputData.getString(PACKAGE_NAME)
val displayName = inputData.getString(DISPLAY_NAME) val displayName = inputData.getString(DISPLAY_NAME)
val versionCode = inputData.getInt(VERSION_CODE, -1) val versionCode = inputData.getLong(VERSION_CODE, -1)
notificationManager = context.getSystemService<NotificationManager>()!! notificationManager = context.getSystemService<NotificationManager>()!!
if (packageName.isNullOrEmpty() || isDownload && versionCode == -1) { if (packageName.isNullOrEmpty() || isDownload && versionCode == -1L) {
Log.e(TAG, "Input data is corrupt, bailing out!") Log.e(TAG, "Input data is corrupt, bailing out!")
notifyStatus(displayName ?: String(), uri, false) notifyStatus(displayName ?: String(), uri, false)
return Result.failure() return Result.failure()
@@ -156,7 +156,7 @@ class ExportWorker @AssistedInject constructor(
bundleAllAPKs(fileList.filterNotNull(), uri) bundleAllAPKs(fileList.filterNotNull(), uri)
} }
private fun copyDownloadedApp(packageName: String, versionCode: Int, uri: Uri) { private fun copyDownloadedApp(packageName: String, versionCode: Long, uri: Uri) {
return bundleAllAPKs( return bundleAllAPKs(
PathUtil.getAppDownloadDir(context, packageName, versionCode).listFiles()!!.toList(), PathUtil.getAppDownloadDir(context, packageName, versionCode).listFiles()!!.toList(),
uri uri

View File

@@ -51,8 +51,8 @@ object PackageUtil {
private const val TAG = "PackageUtil" private const val TAG = "PackageUtil"
const val PACKAGE_NAME_GMS = "com.google.android.gms" const val PACKAGE_NAME_GMS = "com.google.android.gms"
private const val VERSION_CODE_MICRO_G = 240913402 private const val VERSION_CODE_MICRO_G: Long = 240913402
private const val VERSION_CODE_MICRO_G_HUAWEI = 240913007 private const val VERSION_CODE_MICRO_G_HUAWEI: Long = 240913007
fun getAllValidPackages(context: Context): List<PackageInfo> { fun getAllValidPackages(context: Context): List<PackageInfo> {
return context.packageManager.getInstalledPackages(PackageManager.GET_META_DATA) return context.packageManager.getInstalledPackages(PackageManager.GET_META_DATA)
@@ -85,7 +85,7 @@ object PackageUtil {
} }
} }
fun isInstalled(context: Context, packageName: String, versionCode: Int): Boolean { fun isInstalled(context: Context, packageName: String, versionCode: Long): Boolean {
return try { return try {
val packageInfo = getPackageInfo(context, packageName) val packageInfo = getPackageInfo(context, packageName)
return PackageInfoCompat.getLongVersionCode(packageInfo) >= versionCode.toLong() return PackageInfoCompat.getLongVersionCode(packageInfo) >= versionCode.toLong()
@@ -110,17 +110,17 @@ object PackageUtil {
} }
} }
fun isSharedLibraryInstalled(context: Context, packageName: String, versionCode: Int): Boolean { fun isSharedLibraryInstalled(context: Context, packageName: String, versionCode: Long): Boolean {
return if (isOAndAbove) { return if (isOAndAbove) {
val sharedLibraries = getAllSharedLibraries(context) val sharedLibraries = getAllSharedLibraries(context)
if (isPAndAbove) { if (isPAndAbove) {
sharedLibraries.any { sharedLibraries.any {
it.name == packageName && it.longVersion == versionCode.toLong() it.name == packageName && it.longVersion == versionCode
} }
} else { } else {
sharedLibraries.any { sharedLibraries.any {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
it.name == packageName && it.version == versionCode it.name == packageName && it.version == versionCode.toInt()
} }
} }
} else { } else {

View File

@@ -21,10 +21,10 @@ package com.aurora.store.util
import android.content.Context import android.content.Context
import android.os.Environment import android.os.Environment
import com.aurora.gplayapi.data.models.PlayFile
import com.aurora.store.data.room.download.Download import com.aurora.store.data.room.download.Download
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
import com.aurora.gplayapi.data.models.File as GPlayFile
object PathUtil { object PathUtil {
@@ -47,14 +47,14 @@ object PathUtil {
return File(getDownloadDirectory(context), packageName) return File(getDownloadDirectory(context), packageName)
} }
fun getAppDownloadDir(context: Context, packageName: String, versionCode: Int): File { fun getAppDownloadDir(context: Context, packageName: String, versionCode: Long): File {
return File(getPackageDirectory(context, packageName), versionCode.toString()) return File(getPackageDirectory(context, packageName), versionCode.toString())
} }
fun getLibDownloadDir( fun getLibDownloadDir(
context: Context, context: Context,
packageName: String, packageName: String,
versionCode: Int, versionCode: Long,
sharedLibPackageName: String sharedLibPackageName: String
): File { ): File {
return File( return File(
@@ -64,15 +64,15 @@ object PathUtil {
} }
/** /**
* Returns an instance of java's [File] class for the given [GPlayFile] * Returns an instance of java's [File] class for the given [PlayFile]
* @param context [Context] * @param context [Context]
* @param gFile [GPlayFile] to download * @param playFile [PlayFile] to download
* @param download An instance of [Download] * @param download An instance of [Download]
*/ */
fun getLocalFile(context: Context, gFile: GPlayFile, download: Download): File { fun getLocalFile(context: Context, playFile: PlayFile, download: Download): File {
val sharedLib = download.sharedLibs.find { it.fileList.contains(gFile) } val sharedLib = download.sharedLibs.find { it.fileList.contains(playFile) }
return when (gFile.type) { return when (playFile.type) {
GPlayFile.FileType.BASE, GPlayFile.FileType.SPLIT -> { PlayFile.Type.BASE, PlayFile.Type.SPLIT -> {
val downloadDir = if (sharedLib != null) { val downloadDir = if (sharedLib != null) {
getLibDownloadDir( getLibDownloadDir(
context, context,
@@ -83,16 +83,16 @@ object PathUtil {
} else { } else {
getAppDownloadDir(context, download.packageName, download.versionCode) getAppDownloadDir(context, download.packageName, download.versionCode)
} }
return File(downloadDir, gFile.name) return File(downloadDir, playFile.name)
} }
GPlayFile.FileType.OBB, GPlayFile.FileType.PATCH -> { PlayFile.Type.OBB, PlayFile.Type.PATCH -> {
File(getObbDownloadDir(download.packageName), gFile.name) File(getObbDownloadDir(download.packageName), playFile.name)
} }
} }
} }
fun getZipFile(context: Context, packageName: String, versionCode: Int): File { fun getZipFile(context: Context, packageName: String, versionCode: Long): File {
return File( return File(
getAppDownloadDir( getAppDownloadDir(
context, context,

View File

@@ -23,7 +23,7 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import com.airbnb.epoxy.ModelProp import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView import com.airbnb.epoxy.ModelView
import com.aurora.gplayapi.data.models.File import com.aurora.gplayapi.data.models.PlayFile
import com.aurora.store.databinding.ViewFileBinding import com.aurora.store.databinding.ViewFileBinding
import com.aurora.store.util.CommonUtil import com.aurora.store.util.CommonUtil
import com.aurora.store.view.epoxy.views.BaseModel import com.aurora.store.view.epoxy.views.BaseModel
@@ -40,7 +40,7 @@ class FileView @JvmOverloads constructor(
) : BaseView<ViewFileBinding>(context, attrs, defStyleAttr) { ) : BaseView<ViewFileBinding>(context, attrs, defStyleAttr) {
@ModelProp @ModelProp
fun file(file: File) { fun file(file: PlayFile) {
binding.line1.text = file.name binding.line1.text = file.name
binding.line2.text = CommonUtil.addSiPrefix(file.size) binding.line2.text = CommonUtil.addSiPrefix(file.size)
} }

View File

@@ -27,8 +27,6 @@ import androidx.navigation.fragment.navArgs
import com.aurora.gplayapi.data.models.App 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.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
@@ -44,14 +42,11 @@ class CategoryBrowseFragment : BaseFragment<FragmentGenericWithToolbarBinding>()
private val args: CategoryBrowseFragmentArgs by navArgs() private val args: CategoryBrowseFragmentArgs by navArgs()
private val viewModel: CategoryStreamViewModel by activityViewModels() private val viewModel: CategoryStreamViewModel by activityViewModels()
private lateinit var category: StreamContract.Category
private var streamBundle: StreamBundle? = StreamBundle() private var streamBundle: StreamBundle? = StreamBundle()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
category = CategoryUtil.getCategoryFromUrl(args.browseUrl)
val genericCarouselController = CategoryCarouselController(this) val genericCarouselController = CategoryCarouselController(this)
// Toolbar // Toolbar
@@ -64,11 +59,11 @@ class CategoryBrowseFragment : BaseFragment<FragmentGenericWithToolbarBinding>()
binding.recycler.setController(genericCarouselController) binding.recycler.setController(genericCarouselController)
binding.recycler.addOnScrollListener(object : EndlessRecyclerOnScrollListener() { binding.recycler.addOnScrollListener(object : EndlessRecyclerOnScrollListener() {
override fun onLoadMore(currentPage: Int) { override fun onLoadMore(currentPage: Int) {
viewModel.observe(category) viewModel.observe(args.browseUrl)
} }
}) })
viewModel.getStreamBundle(category) viewModel.getStreamBundle(args.browseUrl)
viewModel.liveData.observe(viewLifecycleOwner) { viewModel.liveData.observe(viewLifecycleOwner) {
when (it) { when (it) {
is ViewState.Loading -> { is ViewState.Loading -> {
@@ -77,7 +72,7 @@ class CategoryBrowseFragment : BaseFragment<FragmentGenericWithToolbarBinding>()
is ViewState.Success<*> -> { is ViewState.Success<*> -> {
val stash = it.getDataAs<Map<String, StreamBundle>>() val stash = it.getDataAs<Map<String, StreamBundle>>()
streamBundle = stash[category.value] streamBundle = stash[args.browseUrl]
genericCarouselController.setData(streamBundle) genericCarouselController.setData(streamBundle)
} }
@@ -92,7 +87,7 @@ class CategoryBrowseFragment : BaseFragment<FragmentGenericWithToolbarBinding>()
} }
override fun onClusterScrolled(streamCluster: StreamCluster) { override fun onClusterScrolled(streamCluster: StreamCluster) {
viewModel.observeCluster(category, streamCluster) viewModel.observeCluster(args.browseUrl, streamCluster)
} }
override fun onAppClick(app: App) { override fun onAppClick(app: App) {

View File

@@ -136,8 +136,8 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
val requestedApp = app.copy( val requestedApp = app.copy(
versionCode = event.versionCode, versionCode = event.versionCode,
dependencies = app.dependencies.copy( dependencies = app.dependencies.copy(
dependentLibraries = app.dependencies.dependentLibraries.onEach { lib -> dependentLibraries = app.dependencies.dependentLibraries.map { lib ->
lib.versionCode = event.versionCode lib.copy(versionCode = event.versionCode)
} }
) )
) )
@@ -173,11 +173,10 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
app = args.app ?: App(args.packageName) app = args.app ?: App(
app.apply { packageName = args.packageName,
// Check whether app is installed or not
isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName) isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName)
} )
// Show the basic app details, while the rest of the data is being fetched // Show the basic app details, while the rest of the data is being fetched
updateAppHeader(app, false) updateAppHeader(app, false)
@@ -204,7 +203,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
updateExtraDetails(app) updateExtraDetails(app)
updateCompatibilityInfo() updateCompatibilityInfo()
if (app.versionCode == 0) { if (app.versionCode == 0L) {
warnAppUnavailable(app) warnAppUnavailable(app)
} }
@@ -607,7 +606,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
private fun checkAndSetupInstall() { private fun checkAndSetupInstall() {
app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName) app = app.copy(isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName))
// Setup primary and secondary action buttons // Setup primary and secondary action buttons
binding.layoutDetailsApp.btnPrimaryAction.isEnabled = true binding.layoutDetailsApp.btnPrimaryAction.isEnabled = true
@@ -634,7 +633,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
binding.layoutDetailsApp.btnPrimaryAction.apply { binding.layoutDetailsApp.btnPrimaryAction.apply {
text = getString(R.string.action_update) text = getString(R.string.action_update)
setOnClickListener { setOnClickListener {
if (app.versionCode == 0) { if (app.versionCode == 0L) {
toast(R.string.toast_app_unavailable) toast(R.string.toast_app_unavailable)
setText(R.string.status_unavailable) setText(R.string.status_unavailable)
} else { } else {
@@ -692,7 +691,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
if (viewModel.authProvider.isAnonymous && !app.isFree) { if (viewModel.authProvider.isAnonymous && !app.isFree) {
toast(R.string.toast_purchase_blocked) toast(R.string.toast_purchase_blocked)
return@setOnClickListener return@setOnClickListener
} else if (app.versionCode == 0) { } else if (app.versionCode == 0L) {
toast(R.string.toast_app_unavailable) toast(R.string.toast_app_unavailable)
return@setOnClickListener return@setOnClickListener
} }

View File

@@ -95,7 +95,7 @@ class ManualDownloadSheet : BaseDialogSheet<SheetManualDownloadBinding>() {
if (customVersionString.isEmpty()) if (customVersionString.isEmpty())
binding.versionCodeInp.error = "Enter version code" binding.versionCodeInp.error = "Enter version code"
else { else {
viewModel.purchase(args.app, customVersionString.toInt()) viewModel.purchase(args.app, customVersionString.toLong())
} }
} }

View File

@@ -67,10 +67,10 @@ class ExpandedStreamBrowseViewModel @Inject constructor(
streamCluster.clusterNextPageUrl streamCluster.clusterNextPageUrl
) )
streamCluster.apply { streamCluster = streamCluster.copy(
clusterAppList.addAll(newCluster.clusterAppList) clusterAppList = streamCluster.clusterAppList + newCluster.clusterAppList,
clusterNextPageUrl = newCluster.clusterNextPageUrl clusterNextPageUrl = newCluster.clusterNextPageUrl
} )
liveData.postValue(streamCluster) liveData.postValue(streamCluster)

View File

@@ -56,10 +56,10 @@ class StreamBrowseViewModel @Inject constructor(
streamCluster.clusterNextPageUrl streamCluster.clusterNextPageUrl
) )
streamCluster.apply { streamCluster = streamCluster.copy(
clusterAppList.addAll(nextCluster.clusterAppList) clusterNextPageUrl = nextCluster.clusterNextPageUrl,
clusterNextPageUrl = nextCluster.clusterNextPageUrl clusterAppList = streamCluster.clusterAppList + nextCluster.clusterAppList
} )
liveData.postValue(streamCluster) liveData.postValue(streamCluster)
} else { } else {

View File

@@ -96,9 +96,9 @@ class AppDetailsViewModel @Inject constructor(
checkFavourite(packageName) checkFavourite(packageName)
val app: App = appStash.getOrPut(packageName) { val app: App = appStash.getOrPut(packageName) {
appDetailsHelper.getAppByPackageName(packageName).apply { appDetailsHelper.getAppByPackageName(packageName).copy(
isInstalled = PackageUtil.isInstalled(context, packageName) isInstalled = PackageUtil.isInstalled(context, packageName)
} )
} }
_app.emit(app) _app.emit(app)

View File

@@ -62,10 +62,11 @@ class DetailsClusterViewModel @Inject constructor(
if (!bundle.hasCluster() || bundle.hasNext()) { if (!bundle.hasCluster() || bundle.hasNext()) {
val newBundle = appDetailsHelper.getDetailsStream(streamUrl) val newBundle = appDetailsHelper.getDetailsStream(streamUrl)
bundle.apply { val mergedBundle = bundle.copy(
streamClusters.putAll(newBundle.streamClusters) streamClusters = bundle.streamClusters + newBundle.streamClusters,
streamNextPageUrl = newBundle.streamNextPageUrl streamNextPageUrl = newBundle.streamNextPageUrl
} )
stash[streamUrl] = mergedBundle
liveData.postValue(ViewState.Success(stash)) liveData.postValue(ViewState.Success(stash))
} }
@@ -87,7 +88,6 @@ class DetailsClusterViewModel @Inject constructor(
liveData.postValue(ViewState.Success(stash)) liveData.postValue(ViewState.Success(stash))
} else { } else {
Log.i(TAG, "End of cluster") Log.i(TAG, "End of cluster")
streamCluster.clusterNextPageUrl = String()
} }
} catch (e: Exception) { } catch (e: Exception) {
liveData.postValue(ViewState.Error(e.message)) liveData.postValue(ViewState.Error(e.message))
@@ -101,9 +101,18 @@ class DetailsClusterViewModel @Inject constructor(
clusterID: Int, clusterID: Int,
newCluster: StreamCluster newCluster: StreamCluster
) { ) {
targetBundle(url).streamClusters[clusterID]?.apply { val bundle = targetBundle(url)
clusterAppList.addAll(newCluster.clusterAppList) bundle.streamClusters[clusterID]?.let { oldCluster ->
clusterNextPageUrl = newCluster.clusterNextPageUrl val mergedCluster = oldCluster.copy(
clusterNextPageUrl = newCluster.clusterNextPageUrl,
clusterAppList = oldCluster.clusterAppList + newCluster.clusterAppList
)
val newStreamClusters = bundle.streamClusters.toMutableMap().also {
it.remove(clusterID)
it[clusterID] = mergedCluster
}
stash.put(url, bundle.copy(streamClusters = newStreamClusters))
} }
} }

View File

@@ -76,7 +76,6 @@ class DevProfileViewModel @Inject constructor(
liveData.postValue(ViewState.Success(devStream)) liveData.postValue(ViewState.Success(devStream))
} else { } else {
Log.i(TAG, "End of cluster") Log.i(TAG, "End of cluster")
streamCluster.clusterNextPageUrl = String()
} }
} catch (e: Exception) { } catch (e: Exception) {
liveData.postValue(ViewState.Error(e.message)) liveData.postValue(ViewState.Error(e.message))
@@ -86,9 +85,17 @@ class DevProfileViewModel @Inject constructor(
} }
private fun updateCluster(newCluster: StreamCluster) { private fun updateCluster(newCluster: StreamCluster) {
streamBundle.streamClusters[newCluster.id]?.apply { streamBundle.streamClusters[newCluster.id]?.let { oldCluster ->
clusterAppList.addAll(newCluster.clusterAppList) val mergedCluster = oldCluster.copy(
clusterNextPageUrl = newCluster.clusterNextPageUrl clusterNextPageUrl = newCluster.clusterNextPageUrl,
clusterAppList = oldCluster.clusterAppList + newCluster.clusterAppList
)
val newStreamClusters = streamBundle.streamClusters.toMutableMap().also {
it.remove(newCluster.id)
it[newCluster.id] = mergedCluster
}
streamBundle = streamBundle.copy(streamClusters = newStreamClusters)
} }
} }
} }

View File

@@ -76,10 +76,11 @@ class StreamViewModel @Inject constructor(
} }
//Update old bundle //Update old bundle
bundle.apply { val mergedBundle = bundle.copy(
streamClusters.putAll(newBundle.streamClusters) streamClusters = bundle.streamClusters + newBundle.streamClusters,
streamNextPageUrl = newBundle.streamNextPageUrl streamNextPageUrl = newBundle.streamNextPageUrl
} )
stash[category] = mergedBundle
//Post updated to UI //Post updated to UI
liveData.postValue(ViewState.Success(stash)) liveData.postValue(ViewState.Success(stash))
@@ -98,13 +99,13 @@ class StreamViewModel @Inject constructor(
supervisorScope { supervisorScope {
try { try {
if (streamCluster.hasNext()) { if (streamCluster.hasNext()) {
val newCluster = val newCluster = streamContract.nextStreamCluster(
streamContract.nextStreamCluster(streamCluster.clusterNextPageUrl) streamCluster.clusterNextPageUrl
)
updateCluster(category, streamCluster.id, newCluster) updateCluster(category, streamCluster.id, newCluster)
liveData.postValue(ViewState.Success(stash)) liveData.postValue(ViewState.Success(stash))
} else { } else {
Log.i(TAG, "End of cluster") Log.i(TAG, "End of cluster")
streamCluster.clusterNextPageUrl = String()
} }
} catch (e: Exception) { } catch (e: Exception) {
liveData.postValue(ViewState.Error(e.message)) liveData.postValue(ViewState.Error(e.message))
@@ -118,9 +119,18 @@ class StreamViewModel @Inject constructor(
clusterID: Int, clusterID: Int,
newCluster: StreamCluster newCluster: StreamCluster
) { ) {
targetBundle(category).streamClusters[clusterID]?.apply { val bundle = targetBundle(category)
clusterAppList.addAll(newCluster.clusterAppList) bundle.streamClusters[clusterID]?.let { oldCluster ->
clusterNextPageUrl = newCluster.clusterNextPageUrl val mergedCluster = oldCluster.copy(
clusterNextPageUrl = newCluster.clusterNextPageUrl,
clusterAppList = oldCluster.clusterAppList + newCluster.clusterAppList
)
val newStreamClusters = bundle.streamClusters.toMutableMap().also {
it.remove(clusterID)
it[clusterID] = mergedCluster
}
stash.put(category, bundle.copy(streamClusters = newStreamClusters))
} }
} }

View File

@@ -57,11 +57,10 @@ class ReviewViewModel @Inject constructor(
supervisorScope { supervisorScope {
try { try {
val nextReviewCluster = reviewsHelper.next(nextReviewPageUrl) val nextReviewCluster = reviewsHelper.next(nextReviewPageUrl)
reviewsCluster.copy( reviewsCluster = reviewsCluster.copy(
nextPageUrl = nextReviewCluster.nextPageUrl nextPageUrl = nextReviewCluster.nextPageUrl,
).apply { reviewList = nextReviewCluster.reviewList + nextReviewCluster.reviewList
reviewList.addAll(nextReviewCluster.reviewList) )
}
liveData.postValue(reviewsCluster) liveData.postValue(reviewsCluster)
} catch (_: Exception) { } catch (_: Exception) {

View File

@@ -23,7 +23,6 @@ import android.util.Log
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.aurora.extensions.flushAndAdd
import com.aurora.gplayapi.data.models.SearchBundle import com.aurora.gplayapi.data.models.SearchBundle
import com.aurora.gplayapi.helpers.SearchHelper import com.aurora.gplayapi.helpers.SearchHelper
import com.aurora.gplayapi.helpers.contracts.SearchContract import com.aurora.gplayapi.helpers.contracts.SearchContract
@@ -54,10 +53,6 @@ class SearchResultViewModel @Inject constructor(
get() = if (authProvider.isAnonymous) webSearchHelper else searchHelper get() = if (authProvider.isAnonymous) webSearchHelper else searchHelper
fun observeSearchResults(query: String) { fun observeSearchResults(query: String) {
//Clear old results
searchBundle.subBundles.clear()
searchBundle.appList.clear()
//Fetch new results
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
supervisorScope { supervisorScope {
try { try {
@@ -75,17 +70,17 @@ class SearchResultViewModel @Inject constructor(
} }
@Synchronized @Synchronized
fun next(nextSubBundleSet: MutableSet<SearchBundle.SubBundle>) { fun next(nextSubBundleSet: Set<SearchBundle.SubBundle>) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
supervisorScope { supervisorScope {
try { try {
if (nextSubBundleSet.isNotEmpty()) { if (nextSubBundleSet.isNotEmpty()) {
val newSearchBundle = helper.next(nextSubBundleSet) val newSearchBundle = helper.next(nextSubBundleSet.toMutableSet())
if (newSearchBundle.appList.isNotEmpty()) { if (newSearchBundle.appList.isNotEmpty()) {
searchBundle.apply { searchBundle = searchBundle.copy(
subBundles.flushAndAdd(newSearchBundle.subBundles) subBundles = newSearchBundle.subBundles,
appList.addAll(newSearchBundle.appList) appList = searchBundle.appList + newSearchBundle.appList
} )
liveData.postValue(searchBundle) liveData.postValue(searchBundle)
} }

View File

@@ -24,7 +24,7 @@ class ManualDownloadViewModel @Inject constructor(
private val _purchaseStatus = MutableSharedFlow<Boolean>() private val _purchaseStatus = MutableSharedFlow<Boolean>()
val purchaseStatus = _purchaseStatus.asSharedFlow() val purchaseStatus = _purchaseStatus.asSharedFlow()
fun purchase(app: App, customVersion: Int) { fun purchase(app: App, customVersion: Long) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val files = purchaseHelper.purchase(app.packageName, customVersion, app.offerType) val files = purchaseHelper.purchase(app.packageName, customVersion, app.offerType)

View File

@@ -49,38 +49,38 @@ class CategoryStreamViewModel @Inject constructor(
private val categoryStreamContract: CategoryStreamContract private val categoryStreamContract: CategoryStreamContract
get() = webCategoryStreamHelper get() = webCategoryStreamHelper
fun getStreamBundle(category: StreamContract.Category) { fun getStreamBundle(browseUrl: String) {
liveData.postValue(ViewState.Loading) liveData.postValue(ViewState.Loading)
observe(category) observe(browseUrl)
} }
fun observe(category: StreamContract.Category) { fun observe(browseUrl: String) {
liveData.postValue(ViewState.Loading) liveData.postValue(ViewState.Loading)
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
supervisorScope { supervisorScope {
val bundle = targetBundle(category) val bundle = targetBundle(browseUrl)
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 = if (bundle.streamClusters.isEmpty()) { val newBundle = if (bundle.streamClusters.isEmpty()) {
categoryStreamContract.fetch(category.value) categoryStreamContract.fetch(browseUrl)
} else { } else {
categoryStreamContract.nextStreamBundle( categoryStreamContract.nextStreamBundle(
category, StreamContract.Category.NONE,
bundle.streamNextPageUrl bundle.streamNextPageUrl
) )
} }
//Update old bundle //Update old bundle
bundle.apply { val mergedBundle = bundle.copy(
streamClusters.putAll(newBundle.streamClusters) streamClusters = bundle.streamClusters + newBundle.streamClusters,
streamNextPageUrl = newBundle.streamNextPageUrl streamNextPageUrl = newBundle.streamNextPageUrl
} )
stash[browseUrl] = mergedBundle
//Post updated to UI //Post updated to UI
liveData.postValue(ViewState.Success(stash)) liveData.postValue(ViewState.Success(stash))
@@ -94,18 +94,18 @@ class CategoryStreamViewModel @Inject constructor(
} }
} }
fun observeCluster(category: StreamContract.Category, streamCluster: StreamCluster) { fun observeCluster(browseUrl: String, streamCluster: StreamCluster) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
supervisorScope { supervisorScope {
try { try {
if (streamCluster.hasNext()) { if (streamCluster.hasNext()) {
val newCluster = val newCluster = categoryStreamContract.nextStreamCluster(
categoryStreamContract.nextStreamCluster(streamCluster.clusterNextPageUrl) streamCluster.clusterNextPageUrl
updateCluster(category, streamCluster.id, newCluster) )
updateCluster(browseUrl, streamCluster.id, newCluster)
liveData.postValue(ViewState.Success(stash)) liveData.postValue(ViewState.Success(stash))
} else { } else {
Log.i(TAG, "End of cluster") Log.i(TAG, "End of cluster")
streamCluster.clusterNextPageUrl = String()
} }
} catch (e: Exception) { } catch (e: Exception) {
liveData.postValue(ViewState.Error(e.message)) liveData.postValue(ViewState.Error(e.message))
@@ -114,22 +114,24 @@ class CategoryStreamViewModel @Inject constructor(
} }
} }
private fun updateCluster( private fun updateCluster(browseUrl: String, clusterID: Int, newCluster: StreamCluster) {
category: StreamContract.Category, val bundle = targetBundle(browseUrl)
clusterID: Int, bundle.streamClusters[clusterID]?.let { oldCluster ->
newCluster: StreamCluster val mergedCluster = oldCluster.copy(
) { clusterNextPageUrl = newCluster.clusterNextPageUrl,
targetBundle(category).streamClusters[clusterID]?.apply { clusterAppList = oldCluster.clusterAppList + newCluster.clusterAppList
clusterAppList.addAll(newCluster.clusterAppList) )
clusterNextPageUrl = newCluster.clusterNextPageUrl val newStreamClusters = bundle.streamClusters.toMutableMap().also {
it.remove(clusterID)
it[clusterID] = mergedCluster
}
stash.put(browseUrl, bundle.copy(streamClusters = newStreamClusters))
} }
} }
private fun targetBundle(category: StreamContract.Category): StreamBundle { private fun targetBundle(browseUrl: String): StreamBundle {
val streamBundle = stash.getOrPut(category.value) { val streamBundle = stash.getOrPut(browseUrl) { StreamBundle() }
StreamBundle()
}
return streamBundle return streamBundle
} }
} }

View File

@@ -85,10 +85,13 @@ class TopChartViewModel @Inject constructor(
chart: TopChartsContract.Chart, chart: TopChartsContract.Chart,
newCluster: StreamCluster newCluster: StreamCluster
) { ) {
targetCluster(type, chart).apply { val streamCluster = targetCluster(type, chart)
clusterAppList.addAll(newCluster.clusterAppList) val mergedCluster = streamCluster.copy(
clusterNextPageUrl = newCluster.clusterNextPageUrl clusterNextPageUrl = newCluster.clusterNextPageUrl,
} clusterAppList = streamCluster.clusterAppList + newCluster.clusterAppList
)
stash[type] = mutableMapOf(chart to mergedCluster)
} }
private fun targetCluster( private fun targetCluster(

View File

@@ -9,7 +9,7 @@ composeBom = "2025.05.00"
core = "1.16.0" core = "1.16.0"
epoxy = "5.1.4" epoxy = "5.1.4"
espresso = "3.6.1" espresso = "3.6.1"
gplayapi = "3.4.6" gplayapi = "3.5.0"
gson = "2.13.1" gson = "2.13.1"
hiddenapibypass = "6.1" hiddenapibypass = "6.1"
hilt = "2.56.2" hilt = "2.56.2"