Merge branch 'dev'

This commit is contained in:
Aayush Gupta
2025-10-12 13:32:07 +08:00
9 changed files with 73 additions and 57 deletions

View File

@@ -135,7 +135,7 @@ fun AppDetailsScreen(
exodusReport = exodusReport,
onNavigateUp = onNavigateUp,
onNavigateToAppDetails = onNavigateToAppDetails,
onDownload = { viewModel.purchase(app!!) },
onDownload = { viewModel.enqueueDownload(app!!) },
onFavorite = { viewModel.toggleFavourite(app!!) },
onCancelDownload = { viewModel.cancelDownload(app!!) },
onUninstall = { AppInstaller.uninstall(context, packageName) },
@@ -274,14 +274,14 @@ private fun ScreenContentApp(
@Composable
fun SetupActions() {
when (state) {
is AppState.Queued,
is AppState.Purchasing,
is AppState.Downloading -> {
Actions(
primaryActionDisplayName = stringResource(R.string.action_open),
secondaryActionDisplayName = stringResource(R.string.action_cancel),
isPrimaryActionEnabled = false,
onSecondaryAction = onCancelDownload,
isSecondaryActionEnabled = state !is AppState.Purchasing
onSecondaryAction = onCancelDownload
)
}
@@ -299,7 +299,9 @@ private fun ScreenContentApp(
primaryActionDisplayName = stringResource(R.string.action_open),
secondaryActionDisplayName = stringResource(R.string.action_uninstall),
onPrimaryAction = onOpen,
onSecondaryAction = onUninstall
onSecondaryAction = onUninstall,
isPrimaryActionEnabled = PackageUtil
.getLaunchIntent(context, app.packageName) != null
)
}

View File

@@ -12,9 +12,12 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
@@ -33,7 +36,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
@@ -49,12 +52,12 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.aurora.extensions.adaptiveNavigationIcon
import com.aurora.extensions.isWindowCompact
import com.aurora.extensions.toast
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.compose.composables.InfoComposable
import com.aurora.store.compose.composables.TopAppBarComposable
import com.aurora.store.compose.preview.AppPreviewProvider
import com.aurora.store.data.model.AppState
import com.aurora.store.viewmodel.details.AppDetailsViewModel
import kotlinx.coroutines.android.awaitFrame
import kotlinx.coroutines.launch
@@ -66,26 +69,21 @@ fun ManualDownloadScreen(
viewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
val context = LocalContext.current
val app by viewModel.app.collectAsStateWithLifecycle()
val state by viewModel.state.collectAsStateWithLifecycle()
val topAppBarTitle = when {
windowAdaptiveInfo.isWindowCompact -> app!!.displayName
else -> stringResource(R.string.title_manual_download)
}
LaunchedEffect(key1 = Unit) {
viewModel.purchaseStatus.collect { success ->
if (success) {
context.toast(R.string.toast_manual_available)
onNavigateUp()
} else {
context.toast(R.string.toast_manual_unavailable)
}
LaunchedEffect(key1 = state) {
if (state is AppState.Downloading) {
onNavigateUp()
}
}
ScreenContent(
state = state,
topAppBarTitle = topAppBarTitle,
currentVersionCode = app!!.versionCode,
onNavigateUp = onNavigateUp,
@@ -98,13 +96,14 @@ fun ManualDownloadScreen(
}
)
)
viewModel.purchase(requestedApp)
viewModel.enqueueDownload(requestedApp)
}
)
}
@Composable
private fun ScreenContent(
state: AppState = AppState.Unavailable,
topAppBarTitle: String? = null,
currentVersionCode: Long = 0L,
onNavigateUp: () -> Unit = {},
@@ -116,6 +115,7 @@ private fun ScreenContent(
val snackBarHostState = remember { SnackbarHostState() }
val errorMessage = stringResource(R.string.manual_download_version_error)
val focusManager = LocalFocusManager.current
val focusRequester = remember { FocusRequester() }
var versionCode by remember {
val initText = currentVersionCode.toString()
@@ -157,6 +157,7 @@ private fun ScreenContent(
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
enabled = !state.inProgress(),
value = versionCode,
onValueChange = {
if (it.text.isDigitsOnly()) {
@@ -167,9 +168,16 @@ private fun ScreenContent(
},
shape = RoundedCornerShape(10.dp),
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number
)
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
trailingIcon = {
if (state.inProgress()) {
ContainedLoadingIndicator(
modifier = Modifier
.requiredSize(dimensionResource(R.dimen.icon_size_default))
)
}
}
)
}
@@ -182,7 +190,7 @@ private fun ScreenContent(
onClick = onNavigateUp
) {
Text(
text = stringResource(R.string.action_cancel),
text = stringResource(R.string.action_close),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
@@ -190,7 +198,12 @@ private fun ScreenContent(
Button(
modifier = Modifier.weight(1F),
onClick = { onDownload(versionCode.text.toLong()) }) {
enabled = !state.inProgress(),
onClick = {
onDownload(versionCode.text.toLong())
focusManager.clearFocus()
}
) {
Text(
text = stringResource(R.string.action_install),
maxLines = 1,
@@ -204,9 +217,7 @@ private fun ScreenContent(
@Preview
@Composable
private fun ManualDownloadScreenPreview(
@PreviewParameter(AppPreviewProvider::class) app: App
) {
private fun ManualDownloadScreenPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
ScreenContent(
topAppBarTitle = app.displayName,
currentVersionCode = app.versionCode

View File

@@ -97,9 +97,8 @@ fun Details(
)
}
AppState.Purchasing::class -> {
stringResource(R.string.preparing_to_install)
}
AppState.Queued::class -> stringResource(R.string.status_queued)
AppState.Purchasing::class -> stringResource(R.string.preparing_to_install)
else -> {
stringResource(R.string.version, versionName, versionCode)

View File

@@ -10,10 +10,11 @@ enum class DownloadStatus(@StringRes val localized: Int) {
COMPLETED(R.string.status_completed),
QUEUED(R.string.status_queued),
UNAVAILABLE(R.string.status_unavailable),
VERIFYING(R.string.status_verifying);
VERIFYING(R.string.status_verifying),
PURCHASING(R.string.preparing_to_install);
companion object {
val finished = listOf(FAILED, CANCELLED, COMPLETED)
val running = listOf(QUEUED, DOWNLOADING)
val running = listOf(QUEUED, PURCHASING, DOWNLOADING)
}
}

View File

@@ -55,6 +55,7 @@ sealed class AppState {
val timeRemaining: Long
) : AppState()
data object Queued : AppState()
data object Purchasing : AppState()
data class Installing(val progress: Float) : AppState()
data class Error(val message: String?) : AppState()
@@ -68,7 +69,7 @@ sealed class AppState {
* Whether there is some sort of ongoing process related to the app
*/
fun inProgress(): Boolean {
return this is Downloading || this is Installing || this is Purchasing
return this is Downloading || this is Installing || this is Purchasing || this is Queued
}
/**

View File

@@ -171,8 +171,9 @@ class HttpClient @Inject constructor(private val okHttpClient: OkHttpClient): IH
responseBytes = response.body.bytes(),
errorString = if (!response.isSuccessful) response.message else String()
).also {
val isCached = if (response.cacheResponse != null) "CACHED" else "NETWORK"
_responseCode.value = response.code
Log.i(TAG, "OKHTTP [${response.code}] ${response.request.url}")
Log.i(TAG, "OKHTTP [$isCached] [${response.code}] ${response.request.url}")
}
}
}

View File

@@ -34,9 +34,11 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import okhttp3.Cache
import okhttp3.CertificatePinner
import okhttp3.OkHttpClient
import java.io.ByteArrayInputStream
import java.io.File
import java.io.InputStream
import java.net.Authenticator
import java.net.InetSocketAddress
@@ -59,8 +61,13 @@ object OkHttpClientModule {
@Provides
@Singleton
fun providesOkHttpClientInstance(certPinner: CertificatePinner, proxy: Proxy?): OkHttpClient {
fun providesOkHttpClientInstance(
certificatePinner: CertificatePinner,
proxy: Proxy?,
cache: Cache
): OkHttpClient {
val okHttpClientBuilder = OkHttpClient().newBuilder()
.cache(cache)
.proxy(proxy)
.connectTimeout(25, TimeUnit.SECONDS)
.readTimeout(25, TimeUnit.SECONDS)
@@ -70,7 +77,7 @@ object OkHttpClientModule {
.followSslRedirects(true)
if (!BuildConfig.DEBUG) {
okHttpClientBuilder.certificatePinner(certPinner)
okHttpClientBuilder.certificatePinner(certificatePinner)
}
return okHttpClientBuilder.build()
@@ -122,6 +129,15 @@ object OkHttpClientModule {
}
}
@Provides
@Singleton
fun providesCacheDir(@ApplicationContext context: Context): Cache {
return Cache(
directory = File(context.cacheDir, "http_cache"),
maxSize = 100L * 1024 * 1024
)
}
private fun getGoogleRootCertHashes(context: Context): List<String> {
return try {
val certs = getX509Certificates(context.resources.openRawResource(R.raw.google_roots_ca))

View File

@@ -111,6 +111,7 @@ class DownloadWorker @AssistedInject constructor(
setForeground(getForegroundInfo())
// Try to purchase the app if file list is empty
notifyStatus(DownloadStatus.PURCHASING)
download.fileList = download.fileList.ifEmpty {
purchase(download.packageName, download.versionCode, download.offerType)
}

View File

@@ -17,7 +17,6 @@ import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.data.models.details.TestingProgramStatus
import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.gplayapi.helpers.PurchaseHelper
import com.aurora.gplayapi.helpers.ReviewsHelper
import com.aurora.gplayapi.helpers.web.WebDataSafetyHelper
import com.aurora.gplayapi.network.IHttpClient
@@ -26,6 +25,7 @@ import com.aurora.store.BuildConfig
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.helper.DownloadHelper
import com.aurora.store.data.model.AppState
import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.model.ExodusReport
import com.aurora.store.data.model.PlexusReport
import com.aurora.store.data.model.Report
@@ -40,11 +40,8 @@ import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_EXTENDED
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
@@ -62,7 +59,6 @@ import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport
class AppDetailsViewModel @Inject constructor(
val authProvider: AuthProvider,
@ApplicationContext private val context: Context,
private val purchaseHelper: PurchaseHelper,
private val appDetailsHelper: AppDetailsHelper,
private val reviewsHelper: ReviewsHelper,
private val webDataSafetyHelper: WebDataSafetyHelper,
@@ -104,9 +100,6 @@ class AppDetailsViewModel @Inject constructor(
private val _favourite = MutableStateFlow(false)
val favourite = _favourite.asStateFlow()
private val _purchaseStatus = MutableSharedFlow<Boolean>()
val purchaseStatus = _purchaseStatus.asSharedFlow()
private val download = combine(app, downloadHelper.downloadsList) { a, list ->
if (a?.packageName.isNullOrBlank()) return@combine null
list.find { d -> d.packageName == a.packageName }
@@ -219,20 +212,9 @@ class AppDetailsViewModel @Inject constructor(
}
}
fun purchase(app: App) {
fun enqueueDownload(app: App) {
viewModelScope.launch(Dispatchers.IO) {
try {
_state.value = AppState.Purchasing
val files = purchaseHelper.purchase(app.packageName, app.versionCode, app.offerType)
_purchaseStatus.emit(files.isNotEmpty())
if (files.isNotEmpty()) {
downloadHelper.enqueueApp(app.copy(fileList = files.toMutableList()))
}
} catch (exception: Exception) {
_state.value = defaultAppState
_purchaseStatus.emit(false)
Log.e(TAG, "Failed to purchase the app", exception)
}
downloadHelper.enqueueApp(app)
}
}
@@ -271,13 +253,15 @@ class AppDetailsViewModel @Inject constructor(
}.launchIn(viewModelScope)
download.filterNotNull().onEach {
_state.value = when {
it.isRunning -> AppState.Downloading(
_state.value = when (it.status) {
DownloadStatus.DOWNLOADING -> AppState.Downloading(
it.progress.toFloat(),
it.speed,
it.timeRemaining
)
DownloadStatus.QUEUED -> AppState.Queued
DownloadStatus.PURCHASING -> AppState.Purchasing
else -> defaultAppState
}
}.launchIn(viewModelScope)