Loading app/src/main/java/foundation/e/apps/data/application/data/Application.kt +6 −0 Original line number Diff line number Diff line Loading @@ -102,6 +102,12 @@ data class Application( @SerializedName(value = "antifeatures") val antiFeatures: List<Map<String, String>> = emptyList(), var isSystemApp: Boolean = false, /* * True when Play Store dependency metadata was loaded from a full details response. * An empty dependentLibraries list then means "no libraries", not "unknown". */ val areDependentLibrariesResolved: Boolean = false, val dependentLibraries: List<SharedLib> = emptyList(), ) { val iconUrl: String? Loading app/src/main/java/foundation/e/apps/data/application/utils/GplayApiExtensions.kt +5 −1 Original line number Diff line number Diff line Loading @@ -28,7 +28,10 @@ import foundation.e.apps.data.application.data.Ratings import foundation.e.apps.data.installation.model.SharedLib import foundation.e.apps.data.application.data.Category as AppLoungeCategory fun App.toApplication(context: Context): Application { fun App.toApplication( context: Context, areDependentLibrariesResolved: Boolean = false, ): Application { val app = Application( _id = this.id.toString(), author = this.developerName, Loading Loading @@ -58,6 +61,7 @@ fun App.toApplication(context: Context): Application { price = this.price, restriction = this.restriction, contentRating = this.contentRating, areDependentLibrariesResolved = areDependentLibrariesResolved, dependentLibraries = this.dependencies.dependentLibraries.map { SharedLib(packageName = it.packageName, versionCode = it.versionCode, offerType = it.offerType) } Loading app/src/main/java/foundation/e/apps/data/install/core/AppInstallEnqueueResult.kt 0 → 100644 +43 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program 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 3 of the License, or * (at your option) any later version. * * This program 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 this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.data.install.core import foundation.e.apps.data.enums.ResultStatus sealed interface AppInstallEnqueueResult { data object Enqueued : AppInstallEnqueueResult data class Failed( val reason: AppInstallEnqueueFailureReason, ) : AppInstallEnqueueResult } sealed interface AppInstallEnqueueFailureReason { data class SharedLibraryDetailsUnavailable( val status: ResultStatus? = null, val cause: Throwable? = null, ) : AppInstallEnqueueFailureReason data class PlayStoreInstallMetadataUnavailable( val status: ResultStatus? = null, val cause: Throwable? = null, ) : AppInstallEnqueueFailureReason data object EnqueueRejected : AppInstallEnqueueFailureReason } app/src/main/java/foundation/e/apps/data/install/core/AppInstallationFacade.kt +20 −91 Original line number Diff line number Diff line Loading @@ -19,15 +19,12 @@ package foundation.e.apps.data.install.core import foundation.e.apps.data.application.AppManager import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Source import foundation.e.apps.data.installation.core.InstallationProcessor import foundation.e.apps.data.installation.model.AppInstall import foundation.e.apps.data.installation.model.InstallationResult import foundation.e.apps.domain.model.install.Status import timber.log.Timber import javax.inject.Inject class AppInstallationFacade @Inject constructor( Loading @@ -35,7 +32,7 @@ class AppInstallationFacade @Inject constructor( private val installationEnqueuer: InstallationEnqueuer, private val installationProcessor: InstallationProcessor, private val installationRequest: InstallationRequest, private val applicationRepository: ApplicationRepository, private val playInstallMetadataResolver: PlayInstallMetadataResolver, ) { /** * creates [AppInstall] from [Application] and enqueues into WorkManager to run install process. Loading @@ -48,74 +45,33 @@ class AppInstallationFacade @Inject constructor( isAnUpdate: Boolean = false ): Boolean { return when (val result = initAppInstallWithResult(application, isAnUpdate)) { is AppInstallStartResult.Started -> result.didEnqueue is AppInstallStartResult.Failed -> false AppInstallEnqueueResult.Enqueued -> true is AppInstallEnqueueResult.Failed -> false } } suspend fun initAppInstallWithResult( application: Application, isAnUpdate: Boolean = false ): AppInstallStartResult { val isUpdate = isAnUpdate || application.status == Status.UPDATABLE val appInstall = installationRequest.create(application, isUpdateRequest = isUpdate) if (application.source == Source.PLAY_STORE) { when (val sharedLibrariesResult = resolveSharedLibraries(application)) { is SharedLibrariesResult.Success -> { appInstall.sharedLibs = sharedLibrariesResult.libraries } is SharedLibrariesResult.Failure -> { return AppInstallStartResult.Failed(sharedLibrariesResult.reason) } } } return AppInstallStartResult.Started( enqueueAppForInstallation(appInstall, isUpdate, application.isSystemApp) ) } private suspend fun resolveSharedLibraries( application: Application, ): SharedLibrariesResult { if (application.dependentLibraries.isNotEmpty()) { return SharedLibrariesResult.Success(application.dependentLibraries) } val packageName = application.package_name return runCatching { applicationRepository.getApplicationDetails( packageName, Source.PLAY_STORE ) }.fold( onSuccess = { (details, status) -> if (status == ResultStatus.OK) { SharedLibrariesResult.Success(details.dependentLibraries) ): AppInstallEnqueueResult { val playInstallMetadata = when (val result = playInstallMetadataResolver.resolve(application)) { is PlayInstallMetadataResult.Failure -> return AppInstallEnqueueResult.Failed(result.reason) is PlayInstallMetadataResult.Success -> result.metadata } val installApplication = playInstallMetadata.application val isUpdate = isAnUpdate || application.status == Status.UPDATABLE || installApplication.status == Status.UPDATABLE val appInstall = installationRequest.create(installApplication, isUpdateRequest = isUpdate) appInstall.sharedLibs = playInstallMetadata.sharedLibraries val didEnqueue = enqueueAppForInstallation(appInstall, isUpdate, installApplication.isSystemApp) return if (didEnqueue) { AppInstallEnqueueResult.Enqueued } else { Timber.w( "Cannot install %s: failed to fetch required shared library details (status=%s)", packageName, status, ) SharedLibrariesResult.Failure( AppInstallFailureReason.SharedLibraryDetailsUnavailable(status = status) ) AppInstallEnqueueResult.Failed(AppInstallEnqueueFailureReason.EnqueueRejected) } }, onFailure = { throwable -> Timber.w( throwable, "Cannot install %s: failed to fetch required shared library details", packageName, ) SharedLibrariesResult.Failure( AppInstallFailureReason.SharedLibraryDetailsUnavailable(cause = throwable) ) }, ) } /** Loading Loading @@ -159,30 +115,3 @@ class AppInstallationFacade @Inject constructor( appManager.cancelDownload(appInstall) } } private sealed interface SharedLibrariesResult { data class Success( val libraries: List<foundation.e.apps.data.installation.model.SharedLib>, ) : SharedLibrariesResult data class Failure( val reason: AppInstallFailureReason, ) : SharedLibrariesResult } sealed interface AppInstallStartResult { data class Started( val didEnqueue: Boolean, ) : AppInstallStartResult data class Failed( val reason: AppInstallFailureReason, ) : AppInstallStartResult } sealed interface AppInstallFailureReason { data class SharedLibraryDetailsUnavailable( val status: ResultStatus? = null, val cause: Throwable? = null, ) : AppInstallFailureReason } app/src/main/java/foundation/e/apps/data/install/core/PlayInstallMetadataResolver.kt 0 → 100644 +174 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program 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 3 of the License, or * (at your option) any later version. * * This program 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 this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.data.install.core import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Source import foundation.e.apps.data.installation.model.SharedLib import foundation.e.apps.domain.model.install.Status import kotlinx.coroutines.CancellationException import timber.log.Timber import javax.inject.Inject class PlayInstallMetadataResolver @Inject constructor( private val applicationRepository: ApplicationRepository, ) { suspend fun resolve(application: Application): PlayInstallMetadataResult { val playDetails = when (val result = fetchRequiredPlayDetails(application)) { is MetadataStepResult.Failure -> return PlayInstallMetadataResult.Failure(result.reason) is MetadataStepResult.Success -> result.value } val installApplication = application.prepareInstallApplication(playDetails) val sharedLibraries = application.resolveSharedLibraries(playDetails) return PlayInstallMetadataResult.Success( PlayInstallMetadata( application = installApplication, sharedLibraries = sharedLibraries, ) ) } private suspend fun fetchRequiredPlayDetails( application: Application, ): MetadataStepResult<Application?> { if (application.source != Source.PLAY_STORE || !application.needsPlayDetails()) { return MetadataStepResult.Success(null) } val packageName = application.package_name return runCatching { applicationRepository.getApplicationDetails(packageName, Source.PLAY_STORE) }.fold( onSuccess = { (details, status) -> if (status != ResultStatus.OK) { Timber.w( "Cannot install %s: failed to fetch Play Store install details (status=%s)", packageName, status, ) MetadataStepResult.Failure( application.playDetailsFailureReason(status = status) ) } else { MetadataStepResult.Success(details) } }, onFailure = { throwable -> if (throwable is CancellationException) { throw throwable } Timber.w( throwable, "Cannot install %s: failed to fetch Play Store install details", packageName, ) MetadataStepResult.Failure( application.playDetailsFailureReason(cause = throwable) ) }, ) } private fun Application.needsPlayDetails(): Boolean { return needsPlayStoreInstallMetadata() || needsSharedLibraryDetails() } private fun Application.needsPlayStoreInstallMetadata(): Boolean { return latest_version_code <= 0L || offer_type <= 0 } private fun Application.needsSharedLibraryDetails(): Boolean { return dependentLibraries.isEmpty() && !areDependentLibrariesResolved } private fun Application.playDetailsFailureReason( status: ResultStatus? = null, cause: Throwable? = null, ): AppInstallEnqueueFailureReason { return if (needsPlayStoreInstallMetadata()) { AppInstallEnqueueFailureReason.PlayStoreInstallMetadataUnavailable( status = status, cause = cause, ) } else { AppInstallEnqueueFailureReason.SharedLibraryDetailsUnavailable( status = status, cause = cause, ) } } private fun Status.preserveUpdatableStatusFrom(originalStatus: Status): Status { return if (originalStatus == Status.UPDATABLE) { originalStatus } else { this } } private fun Application.prepareInstallApplication(playDetails: Application?): Application { if (source != Source.PLAY_STORE || playDetails == null) { return this } return playDetails.copy( isPurchased = isPurchased || playDetails.isPurchased, isSystemApp = isSystemApp || playDetails.isSystemApp, status = playDetails.status.preserveUpdatableStatusFrom(status), filterLevel = filterLevel, ) } private fun Application.resolveSharedLibraries(playDetails: Application?): List<SharedLib> { return when { source != Source.PLAY_STORE -> emptyList() dependentLibraries.isNotEmpty() -> dependentLibraries areDependentLibrariesResolved -> emptyList() else -> playDetails?.dependentLibraries.orEmpty() } } } data class PlayInstallMetadata( val application: Application, val sharedLibraries: List<SharedLib>, ) sealed interface PlayInstallMetadataResult { data class Success( val metadata: PlayInstallMetadata, ) : PlayInstallMetadataResult data class Failure( val reason: AppInstallEnqueueFailureReason, ) : PlayInstallMetadataResult } private sealed interface MetadataStepResult<out T> { data class Success<T>( val value: T, ) : MetadataStepResult<T> data class Failure( val reason: AppInstallEnqueueFailureReason, ) : MetadataStepResult<Nothing> } Loading
app/src/main/java/foundation/e/apps/data/application/data/Application.kt +6 −0 Original line number Diff line number Diff line Loading @@ -102,6 +102,12 @@ data class Application( @SerializedName(value = "antifeatures") val antiFeatures: List<Map<String, String>> = emptyList(), var isSystemApp: Boolean = false, /* * True when Play Store dependency metadata was loaded from a full details response. * An empty dependentLibraries list then means "no libraries", not "unknown". */ val areDependentLibrariesResolved: Boolean = false, val dependentLibraries: List<SharedLib> = emptyList(), ) { val iconUrl: String? Loading
app/src/main/java/foundation/e/apps/data/application/utils/GplayApiExtensions.kt +5 −1 Original line number Diff line number Diff line Loading @@ -28,7 +28,10 @@ import foundation.e.apps.data.application.data.Ratings import foundation.e.apps.data.installation.model.SharedLib import foundation.e.apps.data.application.data.Category as AppLoungeCategory fun App.toApplication(context: Context): Application { fun App.toApplication( context: Context, areDependentLibrariesResolved: Boolean = false, ): Application { val app = Application( _id = this.id.toString(), author = this.developerName, Loading Loading @@ -58,6 +61,7 @@ fun App.toApplication(context: Context): Application { price = this.price, restriction = this.restriction, contentRating = this.contentRating, areDependentLibrariesResolved = areDependentLibrariesResolved, dependentLibraries = this.dependencies.dependentLibraries.map { SharedLib(packageName = it.packageName, versionCode = it.versionCode, offerType = it.offerType) } Loading
app/src/main/java/foundation/e/apps/data/install/core/AppInstallEnqueueResult.kt 0 → 100644 +43 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program 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 3 of the License, or * (at your option) any later version. * * This program 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 this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.data.install.core import foundation.e.apps.data.enums.ResultStatus sealed interface AppInstallEnqueueResult { data object Enqueued : AppInstallEnqueueResult data class Failed( val reason: AppInstallEnqueueFailureReason, ) : AppInstallEnqueueResult } sealed interface AppInstallEnqueueFailureReason { data class SharedLibraryDetailsUnavailable( val status: ResultStatus? = null, val cause: Throwable? = null, ) : AppInstallEnqueueFailureReason data class PlayStoreInstallMetadataUnavailable( val status: ResultStatus? = null, val cause: Throwable? = null, ) : AppInstallEnqueueFailureReason data object EnqueueRejected : AppInstallEnqueueFailureReason }
app/src/main/java/foundation/e/apps/data/install/core/AppInstallationFacade.kt +20 −91 Original line number Diff line number Diff line Loading @@ -19,15 +19,12 @@ package foundation.e.apps.data.install.core import foundation.e.apps.data.application.AppManager import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Source import foundation.e.apps.data.installation.core.InstallationProcessor import foundation.e.apps.data.installation.model.AppInstall import foundation.e.apps.data.installation.model.InstallationResult import foundation.e.apps.domain.model.install.Status import timber.log.Timber import javax.inject.Inject class AppInstallationFacade @Inject constructor( Loading @@ -35,7 +32,7 @@ class AppInstallationFacade @Inject constructor( private val installationEnqueuer: InstallationEnqueuer, private val installationProcessor: InstallationProcessor, private val installationRequest: InstallationRequest, private val applicationRepository: ApplicationRepository, private val playInstallMetadataResolver: PlayInstallMetadataResolver, ) { /** * creates [AppInstall] from [Application] and enqueues into WorkManager to run install process. Loading @@ -48,74 +45,33 @@ class AppInstallationFacade @Inject constructor( isAnUpdate: Boolean = false ): Boolean { return when (val result = initAppInstallWithResult(application, isAnUpdate)) { is AppInstallStartResult.Started -> result.didEnqueue is AppInstallStartResult.Failed -> false AppInstallEnqueueResult.Enqueued -> true is AppInstallEnqueueResult.Failed -> false } } suspend fun initAppInstallWithResult( application: Application, isAnUpdate: Boolean = false ): AppInstallStartResult { val isUpdate = isAnUpdate || application.status == Status.UPDATABLE val appInstall = installationRequest.create(application, isUpdateRequest = isUpdate) if (application.source == Source.PLAY_STORE) { when (val sharedLibrariesResult = resolveSharedLibraries(application)) { is SharedLibrariesResult.Success -> { appInstall.sharedLibs = sharedLibrariesResult.libraries } is SharedLibrariesResult.Failure -> { return AppInstallStartResult.Failed(sharedLibrariesResult.reason) } } } return AppInstallStartResult.Started( enqueueAppForInstallation(appInstall, isUpdate, application.isSystemApp) ) } private suspend fun resolveSharedLibraries( application: Application, ): SharedLibrariesResult { if (application.dependentLibraries.isNotEmpty()) { return SharedLibrariesResult.Success(application.dependentLibraries) } val packageName = application.package_name return runCatching { applicationRepository.getApplicationDetails( packageName, Source.PLAY_STORE ) }.fold( onSuccess = { (details, status) -> if (status == ResultStatus.OK) { SharedLibrariesResult.Success(details.dependentLibraries) ): AppInstallEnqueueResult { val playInstallMetadata = when (val result = playInstallMetadataResolver.resolve(application)) { is PlayInstallMetadataResult.Failure -> return AppInstallEnqueueResult.Failed(result.reason) is PlayInstallMetadataResult.Success -> result.metadata } val installApplication = playInstallMetadata.application val isUpdate = isAnUpdate || application.status == Status.UPDATABLE || installApplication.status == Status.UPDATABLE val appInstall = installationRequest.create(installApplication, isUpdateRequest = isUpdate) appInstall.sharedLibs = playInstallMetadata.sharedLibraries val didEnqueue = enqueueAppForInstallation(appInstall, isUpdate, installApplication.isSystemApp) return if (didEnqueue) { AppInstallEnqueueResult.Enqueued } else { Timber.w( "Cannot install %s: failed to fetch required shared library details (status=%s)", packageName, status, ) SharedLibrariesResult.Failure( AppInstallFailureReason.SharedLibraryDetailsUnavailable(status = status) ) AppInstallEnqueueResult.Failed(AppInstallEnqueueFailureReason.EnqueueRejected) } }, onFailure = { throwable -> Timber.w( throwable, "Cannot install %s: failed to fetch required shared library details", packageName, ) SharedLibrariesResult.Failure( AppInstallFailureReason.SharedLibraryDetailsUnavailable(cause = throwable) ) }, ) } /** Loading Loading @@ -159,30 +115,3 @@ class AppInstallationFacade @Inject constructor( appManager.cancelDownload(appInstall) } } private sealed interface SharedLibrariesResult { data class Success( val libraries: List<foundation.e.apps.data.installation.model.SharedLib>, ) : SharedLibrariesResult data class Failure( val reason: AppInstallFailureReason, ) : SharedLibrariesResult } sealed interface AppInstallStartResult { data class Started( val didEnqueue: Boolean, ) : AppInstallStartResult data class Failed( val reason: AppInstallFailureReason, ) : AppInstallStartResult } sealed interface AppInstallFailureReason { data class SharedLibraryDetailsUnavailable( val status: ResultStatus? = null, val cause: Throwable? = null, ) : AppInstallFailureReason }
app/src/main/java/foundation/e/apps/data/install/core/PlayInstallMetadataResolver.kt 0 → 100644 +174 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program 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 3 of the License, or * (at your option) any later version. * * This program 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 this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.data.install.core import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Source import foundation.e.apps.data.installation.model.SharedLib import foundation.e.apps.domain.model.install.Status import kotlinx.coroutines.CancellationException import timber.log.Timber import javax.inject.Inject class PlayInstallMetadataResolver @Inject constructor( private val applicationRepository: ApplicationRepository, ) { suspend fun resolve(application: Application): PlayInstallMetadataResult { val playDetails = when (val result = fetchRequiredPlayDetails(application)) { is MetadataStepResult.Failure -> return PlayInstallMetadataResult.Failure(result.reason) is MetadataStepResult.Success -> result.value } val installApplication = application.prepareInstallApplication(playDetails) val sharedLibraries = application.resolveSharedLibraries(playDetails) return PlayInstallMetadataResult.Success( PlayInstallMetadata( application = installApplication, sharedLibraries = sharedLibraries, ) ) } private suspend fun fetchRequiredPlayDetails( application: Application, ): MetadataStepResult<Application?> { if (application.source != Source.PLAY_STORE || !application.needsPlayDetails()) { return MetadataStepResult.Success(null) } val packageName = application.package_name return runCatching { applicationRepository.getApplicationDetails(packageName, Source.PLAY_STORE) }.fold( onSuccess = { (details, status) -> if (status != ResultStatus.OK) { Timber.w( "Cannot install %s: failed to fetch Play Store install details (status=%s)", packageName, status, ) MetadataStepResult.Failure( application.playDetailsFailureReason(status = status) ) } else { MetadataStepResult.Success(details) } }, onFailure = { throwable -> if (throwable is CancellationException) { throw throwable } Timber.w( throwable, "Cannot install %s: failed to fetch Play Store install details", packageName, ) MetadataStepResult.Failure( application.playDetailsFailureReason(cause = throwable) ) }, ) } private fun Application.needsPlayDetails(): Boolean { return needsPlayStoreInstallMetadata() || needsSharedLibraryDetails() } private fun Application.needsPlayStoreInstallMetadata(): Boolean { return latest_version_code <= 0L || offer_type <= 0 } private fun Application.needsSharedLibraryDetails(): Boolean { return dependentLibraries.isEmpty() && !areDependentLibrariesResolved } private fun Application.playDetailsFailureReason( status: ResultStatus? = null, cause: Throwable? = null, ): AppInstallEnqueueFailureReason { return if (needsPlayStoreInstallMetadata()) { AppInstallEnqueueFailureReason.PlayStoreInstallMetadataUnavailable( status = status, cause = cause, ) } else { AppInstallEnqueueFailureReason.SharedLibraryDetailsUnavailable( status = status, cause = cause, ) } } private fun Status.preserveUpdatableStatusFrom(originalStatus: Status): Status { return if (originalStatus == Status.UPDATABLE) { originalStatus } else { this } } private fun Application.prepareInstallApplication(playDetails: Application?): Application { if (source != Source.PLAY_STORE || playDetails == null) { return this } return playDetails.copy( isPurchased = isPurchased || playDetails.isPurchased, isSystemApp = isSystemApp || playDetails.isSystemApp, status = playDetails.status.preserveUpdatableStatusFrom(status), filterLevel = filterLevel, ) } private fun Application.resolveSharedLibraries(playDetails: Application?): List<SharedLib> { return when { source != Source.PLAY_STORE -> emptyList() dependentLibraries.isNotEmpty() -> dependentLibraries areDependentLibrariesResolved -> emptyList() else -> playDetails?.dependentLibraries.orEmpty() } } } data class PlayInstallMetadata( val application: Application, val sharedLibraries: List<SharedLib>, ) sealed interface PlayInstallMetadataResult { data class Success( val metadata: PlayInstallMetadata, ) : PlayInstallMetadataResult data class Failure( val reason: AppInstallEnqueueFailureReason, ) : PlayInstallMetadataResult } private sealed interface MetadataStepResult<out T> { data class Success<T>( val value: T, ) : MetadataStepResult<T> data class Failure( val reason: AppInstallEnqueueFailureReason, ) : MetadataStepResult<Nothing> }