diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreInstallRoot.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreInstallRoot.kt index 468f9241..a7cb3f15 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreInstallRoot.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreInstallRoot.kt @@ -26,7 +26,80 @@ object StoreInstallRoot { /** One place a game can be installed: the folder, its name for the dialog, and whether it is a card. */ class Target(val root: File, val label: String, val removable: Boolean) { - val freeBytes: Long get() = try { StatFs((if (root.isDirectory) root else root.parentFile ?: root).path).availableBytes } catch (e: Exception) { 0L } + val freeBytes: Long get() = availableBytes(root) + } + + data class StorageCheck( + val enough: Boolean, + val requiredBytes: Long, + val availableBytes: Long, + val internalRequiredBytes: Long = 0L, + val internalAvailableBytes: Long = 0L, + val isInternalShortage: Boolean = false, + val errorMessage: String? = null + ) + + fun availableBytes(file: File): Long = try { + val dir = if (file.isDirectory) file else (file.parentFile ?: file) + if (!dir.exists()) dir.mkdirs() + StatFs(if (dir.exists()) dir.path else file.path).availableBytes + } catch (e: Exception) { + try { file.usableSpace } catch (ignored: Exception) { 0L } + } + + /** + * Estimated bytes needed for an install. Epic on internal storage needs both chunk cache + * and assembled files simultaneously (~2x game size). + */ + fun estimatedBytesNeeded(item: CatalogItem, targetRoot: File, isInternal: Boolean): Long { + val size = StoreSizes.size(item) + if (size <= 0L) return 0L + return if (item.store == Store.EPIC && isInternal) size * 2 else size + } + + /** Pre-install storage check to detect insufficient space before starting. */ + fun checkStorage(context: Context, item: CatalogItem, targetRoot: File): StorageCheck { + val size = StoreSizes.size(item) + val freeTarget = availableBytes(targetRoot) + if (size <= 0L) return StorageCheck(enough = true, requiredBytes = 0L, availableBytes = freeTarget) + val internal = isInternal(context, targetRoot) + if (internal) { + val needed = if (item.store == Store.EPIC) size * 2 else size + if (freeTarget in 1 until needed) { + val msg = if (item.store == Store.EPIC) { + context.getString( + com.droiddeck.launcher.R.string.stores_storage_insufficient, + formatBytes(needed), + formatBytes(freeTarget) + ) + " (" + formatBytes(size) + " + " + formatBytes(size) + " " + context.getString(com.droiddeck.launcher.R.string.stores_stage_download).lowercase() + ")" + } else { + context.getString(com.droiddeck.launcher.R.string.stores_storage_insufficient, formatBytes(needed), formatBytes(freeTarget)) + } + return StorageCheck(enough = false, requiredBytes = needed, availableBytes = freeTarget, errorMessage = msg) + } + return StorageCheck(enough = true, requiredBytes = needed, availableBytes = freeTarget) + } else { + if (freeTarget in 1 until size) { + val msg = context.getString(com.droiddeck.launcher.R.string.stores_storage_insufficient, formatBytes(size), formatBytes(freeTarget)) + return StorageCheck(enough = false, requiredBytes = size, availableBytes = freeTarget, errorMessage = msg) + } + if (item.store == Store.EPIC) { + val internalFree = availableBytes(context.cacheDir) + if (internalFree in 1 until size) { + val msg = context.getString(com.droiddeck.launcher.R.string.stores_storage_internal_cache_insufficient, formatBytes(size), formatBytes(internalFree)) + return StorageCheck( + enough = false, + requiredBytes = size, + availableBytes = freeTarget, + internalRequiredBytes = size, + internalAvailableBytes = internalFree, + isInternalShortage = true, + errorMessage = msg + ) + } + } + return StorageCheck(enough = true, requiredBytes = size, availableBytes = freeTarget) + } } /** The internal root: always there, always the default. */ diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoresState.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoresState.kt index 4f1cb172..5e8caf30 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoresState.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoresState.kt @@ -220,6 +220,30 @@ object StoresState { fun install(context: Context, item: CatalogItem, root: java.io.File? = null) { val app = context.applicationContext val target = root ?: StoreInstallRoot.installRoot(app) + val check = StoreInstallRoot.checkStorage(app, item, target) + if (!check.enough) { + val errorMsg = check.errorMessage ?: app.getString(com.droiddeck.launcher.R.string.stores_storage_insufficient, formatBytes(check.requiredBytes), formatBytes(check.availableBytes)) + val folder = StoreInstallRoot.folderFor(app, item.store, item.id, item.title, target) + val entry = com.droiddeck.launcher.stores.download.DownloadEntry( + item.store, + item.id, + item.title, + cover = item.imageUrl, + bytesTotal = item.sizeBytes, + location = StoreInstallRoot.labelFor(app, folder) + ) + com.droiddeck.launcher.stores.download.SavedQueue.remember(app, item, target) + com.droiddeck.launcher.stores.download.DownloadQueue.recordFailed(app, entry, errorMsg) { + object : com.droiddeck.launcher.stores.download.DownloadQueue.DownloadJob { + override fun run(handle: com.droiddeck.launcher.stores.download.DownloadQueue.JobHandle): String? { + install(app, item, target) + return null + } + override fun cancel(deleteFiles: Boolean) {} + } + } + return + } com.droiddeck.launcher.stores.download.SavedQueue.remember(app, item, target) backends[item.store]?.install(app, item, target) ?: logLine("${item.store.label}: this build cannot install yet") } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadQueue.kt b/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadQueue.kt index 25fd8aa5..4039f0d6 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadQueue.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadQueue.kt @@ -185,6 +185,36 @@ object DownloadQueue { advance(app) } + /** Immediately records a failed download (e.g. from pre-flight storage check). */ + fun recordFailed(context: Context, entry: DownloadEntry, error: String, factory: (() -> DownloadJob)? = null) { + val app = context.applicationContext + val now = System.currentTimeMillis() + synchronized(lock) { + items.remove(entry.key) + items[entry.key] = Item( + entry.copy( + state = DownloadState.FAILED, + error = error, + speedBps = 0, + etaSeconds = -1, + startedAt = now, + finishedAt = now + ), + factory ?: { object : DownloadJob { + override fun run(handle: JobHandle): String? = null + override fun cancel(deleteFiles: Boolean) {} + } } + ) + renumber() + publishLocked() + } + StoresState.logLine("${entry.store.label}: \"${entry.name}\" failed: $error") + main.post { + android.widget.Toast.makeText(app, error, android.widget.Toast.LENGTH_LONG).show() + } + StoreDownloadService.ended(app, entry.copy(state = DownloadState.FAILED, error = error), installed = false, gamesKey = null) + } + fun pause(key: String) { val item = synchronized(lock) { items[key] } ?: return synchronized(lock) { @@ -376,6 +406,10 @@ object DownloadQueue { } DownloadState.FAILED -> { StoresState.logLine("${item.entry.store.label}: \"${item.entry.name}\" failed: $error") + val failMsg = error ?: context.getString(com.droiddeck.launcher.R.string.stores_dl_failed) + main.post { + android.widget.Toast.makeText(context.applicationContext, failMsg, android.widget.Toast.LENGTH_LONG).show() + } StoreDownloadService.ended(context, item.entry, installed = false, gamesKey = null) } DownloadState.CANCELLED -> StoresState.notifyLibraryChanged(context) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadService.kt b/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadService.kt index e8c71f24..2bb7c5cd 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadService.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadService.kt @@ -263,12 +263,16 @@ class StoreDownloadService : Service() { val app = context.applicationContext channels(app) val code = entry.key.hashCode() - val n = Notification.Builder(app, DONE_CHANNEL) + val builder = Notification.Builder(app, DONE_CHANNEL) .setSmallIcon(if (installed) android.R.drawable.stat_sys_download_done else android.R.drawable.stat_notify_error) .setContentTitle(app.getString(if (installed) R.string.stores_notification_installed else R.string.stores_notification_failed, entry.name)) .setAutoCancel(true) .setContentIntent(if (installed && gamesKey != null) openPage(app, gamesKey, code + 10) else openDownloads(app, code + 11)) - .build() + if (!installed && !entry.error.isNullOrBlank()) { + builder.setContentText(entry.error) + builder.style = Notification.BigTextStyle().bigText(entry.error) + } + val n = builder.build() runCatching { app.getSystemService(NotificationManager::class.java)?.notify(ID + 1 + (code and 0xFFFF), n) } } } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicDownloadManager.java b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicDownloadManager.java index ad78a04b..09cc369e 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicDownloadManager.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicDownloadManager.java @@ -224,13 +224,18 @@ public final class EpicDownloadManager { boolean javaPool = true; final int[] pendingIdx = pendingIndices(manifest, pending); boolean engine = com.droiddeck.launcher.stores.StoresNative.INSTANCE.getAvailable() && pendingIdx != null; + String nativeError = null; if (engine) { NativeOutcome r = runNativePool(ctx, manifestBytes, pendingIdx, needed, fTotalBytes, installDirPath, cachePath, cdnUrls, cancel, cb, completedBytes, completedCount, lastSpeedMs, lastSpeedBytes, speedBps); if (r.started) { javaPool = false; if (r.cancelled) return null; - if (!r.success) { cb.onLog("epic: engine failed: " + r.error); failCount.incrementAndGet(); } + if (!r.success) { + cb.onLog("epic: engine failed: " + r.error); + nativeError = r.error; + failCount.incrementAndGet(); + } } else cb.onLog("epic: engine not started (" + r.error + "); using the built-in pool"); } if (javaPool) { @@ -265,7 +270,12 @@ public final class EpicDownloadManager { } } if (cancel.get()) return null; - if (failCount.get() > 0) throw new InstallException(failCount.get() + " chunk(s) failed to download"); + if (failCount.get() > 0) { + if (nativeError != null && !nativeError.isEmpty()) { + throw new InstallException(nativeError); + } + throw new InstallException(failCount.get() + " chunk(s) failed to download"); + } // The engine's assembler when it starts (it drops each chunk after its last use and the // cache at the end); the loop below when it does not. diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresDetail.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresDetail.kt index e77bd70a..2c648697 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresDetail.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresDetail.kt @@ -52,7 +52,9 @@ internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: Fro val id = key.substringAfter(':') val item = (StoresState.library[store].orEmpty() + StoresState.shelves[store]?.all.orEmpty()).firstOrNull { it.id == id } val installed = StoresState.installedGame(store, id) - val download = StoresState.download("${store.id}:$id")?.takeIf { it.isActive } + val rawDownload = StoresState.download("${store.id}:$id") + val download = rawDownload?.takeIf { it.isActive } + val failedDownload = rawDownload?.takeIf { it.state == DownloadState.FAILED } // Removal asks twice: one stray press of A should not cost a download. val title = item?.title ?: installed?.sidecar?.title ?: id Rise(0) { @@ -79,12 +81,24 @@ internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: Fro val state = when { installed != null -> stringResource(R.string.stores_eyebrow_installed) + " · " + com.droiddeck.launcher.stores.StoreInstallRoot.labelFor(ctx, installed.folder) download != null && download.location.isNotBlank() -> stringResource(R.string.stores_eyebrow_library) + " · " + download.location + failedDownload != null && failedDownload.location.isNotBlank() -> stringResource(R.string.stores_eyebrow_library) + " · " + failedDownload.location item?.owned == true -> stringResource(R.string.stores_eyebrow_library) else -> stringResource(R.string.stores_eyebrow_store) } val size = (item?.sizeBytes ?: 0L).takeIf { it > 0 }?.let { " · " + formatBytes(it) } ?: "" Text((state + size).uppercase(), fontSize = 12.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 1.2.sp, color = pal.signal) Text(title, fontSize = if (narrow) 24.sp else 30.sp, lineHeight = if (narrow) 27.sp else 33.sp, fontWeight = FontWeight.Black, color = colors.onBackground, maxLines = 2, overflow = TextOverflow.Ellipsis) + if (!failedDownload?.error.isNullOrBlank()) { + Text( + failedDownload!!.error!!, + fontSize = 13.sp, + lineHeight = 17.sp, + fontWeight = FontWeight.Medium, + color = colors.error, + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + } Actions { when { installed != null -> { @@ -106,6 +120,16 @@ internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: Fro else PrimaryButton(stringResource(R.string.stores_dl_resume), main = true) { com.droiddeck.launcher.stores.download.DownloadQueue.resume(ctx, download.key) } ConfirmButton(stringResource(R.string.stores_dl_cancel), stringResource(R.string.stores_dl_cancel_confirm)) { com.droiddeck.launcher.stores.download.DownloadQueue.cancel(ctx, download.key) } } + failedDownload != null -> { + ActionChip(stringResource(R.string.stores_dl_failed), ok = false) + PrimaryButton(stringResource(R.string.stores_dl_resume), main = true) { + if (item != null) StoresState.requestInstall(ctx, item) + else com.droiddeck.launcher.stores.download.DownloadQueue.retry(ctx, failedDownload.key) + } + ConfirmButton(stringResource(R.string.stores_dl_clear), stringResource(R.string.stores_dl_cancel_confirm), compact = true) { + com.droiddeck.launcher.stores.download.DownloadQueue.clear(ctx, failedDownload.key) + } + } item?.owned == true && StoresState.isUnfinished(item) -> { InstallButton(stringResource(R.string.stores_resume_install)) { StoresState.requestInstall(ctx, item) } ConfirmButton(stringResource(R.string.stores_dl_clear), stringResource(R.string.stores_dl_cancel_confirm), compact = true) { StoresState.clearUnfinished(ctx, item) } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresDownloads.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresDownloads.kt index 47b356e7..520d9fee 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresDownloads.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresDownloads.kt @@ -339,6 +339,7 @@ private fun Modifier.timeRim(left: () -> Float, color: Color): Modifier = drawWi @Composable private fun DownloadCard(d: DownloadEntry, s: FrontEndState, a: FrontEndActions) { val ctx = LocalContext.current + val colors = MaterialTheme.colorScheme val pal = LocalPalette.current val narrow = LocalNarrowPane.current val ink = Color.White @@ -362,8 +363,11 @@ private fun DownloadCard(d: DownloadEntry, s: FrontEndState, a: FrontEndActions) val meta = when (d.state) { DownloadState.INSTALLED -> stringResource(R.string.stores_installed_chip) DownloadState.CANCELLED -> stringResource(R.string.stores_dl_cancelled) - // The reason is in the engine log; the row only says what is kept for Resume. - DownloadState.FAILED -> if (d.bytesDone > 0) stringResource(R.string.stores_dl_failed_kept, formatBytes(d.bytesDone)) else stringResource(R.string.stores_dl_failed) + DownloadState.FAILED -> { + if (!d.error.isNullOrBlank()) stringResource(R.string.stores_dl_failed_reason, d.error) + else if (d.bytesDone > 0) stringResource(R.string.stores_dl_failed_kept, formatBytes(d.bytesDone)) + else stringResource(R.string.stores_dl_failed) + } DownloadState.PAUSED -> stringResource(R.string.stores_dl_paused) DownloadState.QUEUED -> if (d.queuePosition > 0) stringResource(R.string.stores_dl_queued_at, d.queuePosition) else stringResource(R.string.stores_dl_queued) DownloadState.RUNNING -> { @@ -375,7 +379,8 @@ private fun DownloadCard(d: DownloadEntry, s: FrontEndState, a: FrontEndActions) } } } - Text(meta, fontSize = 12.sp, color = dim, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false)) + val metaColor = if (d.state == DownloadState.FAILED) colors.error else dim + Text(meta, fontSize = 12.sp, color = metaColor, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false)) if (d.state == DownloadState.RUNNING && (d.stage == DownloadStage.DOWNLOAD || d.stage == DownloadStage.INSTALL) && d.speedBps > 0) { Text(formatSpeed(d.speedBps), fontSize = 12.sp, color = dim, maxLines = 1) if (d.etaSeconds >= 0) Text(eta(d.etaSeconds), fontSize = 12.sp, color = dim, maxLines = 1) @@ -393,7 +398,10 @@ private fun DownloadCard(d: DownloadEntry, s: FrontEndState, a: FrontEndActions) RoundAction(Icons.Filled.PlayArrow, stringResource(R.string.stores_dl_resume), primary = true) { DownloadQueue.resume(ctx, d.key) } CancelAction { DownloadQueue.cancel(ctx, d.key) } } - DownloadState.FAILED -> RoundAction(Icons.Filled.Refresh, stringResource(R.string.stores_dl_resume), primary = true) { DownloadQueue.retry(ctx, d.key) } + DownloadState.FAILED -> { + RoundAction(Icons.Filled.Refresh, stringResource(R.string.stores_dl_resume), primary = true) { DownloadQueue.retry(ctx, d.key) } + CancelAction { DownloadQueue.clear(ctx, d.key) } + } // Just installed (this row is on its way under the line): Play blooms where the bar gathered. DownloadState.INSTALLED -> Box(Modifier.bloomIn(true, 420)) { RoundAction(Icons.Filled.PlayArrow, stringResource(R.string.stores_play), primary = true) { launchStoreGame(ctx, d.store, d.id, s, a) } @@ -460,6 +468,7 @@ private fun StageBar(d: DownloadEntry) { val x = i * (segW + gap) drawRoundRect(colors.surfaceVariant, Offset(x, cy - h / 2f), Size(segW, h), CornerRadius(h / 2f)) val base = when { + d.state == DownloadState.FAILED -> colors.error done[i] -> pal.good actives[i] && paused -> pausedColor.copy(alpha = breath) actives[i] && stages[i] == DownloadStage.VERIFY -> verifyColor diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresInstallDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresInstallDialog.kt index e7d645a0..4cc53ae2 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresInstallDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresInstallDialog.kt @@ -72,10 +72,18 @@ internal fun InstallWhereDialog(item: CatalogItem, onDismiss: () -> Unit) { Rise(1) { Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) { targets.forEachIndexed { i, t -> + val needed = com.droiddeck.launcher.stores.StoreInstallRoot.estimatedBytesNeeded(item, t.root, !t.removable) + val isLow = needed > 0 && t.freeBytes in 1 until needed + val freeText = if (isLow) { + stringResource(R.string.stores_storage_low, formatBytes(t.freeBytes), formatBytes(needed)) + } else { + stringResource(R.string.stores_target_free, formatBytes(t.freeBytes)) + } TargetRow( label = if (t.removable) t.label else stringResource(R.string.stores_target_internal), - free = stringResource(R.string.stores_target_free, formatBytes(t.freeBytes)), + free = freeText, removable = t.removable, focus = focus[i], + warning = isLow, ) { SessionPrefs.setStoresInstallTarget(ctx, t.root.absolutePath) StoresState.install(ctx, item, t.root) @@ -88,7 +96,7 @@ internal fun InstallWhereDialog(item: CatalogItem, onDismiss: () -> Unit) { } @Composable -private fun TargetRow(label: String, free: String, removable: Boolean, focus: FocusRequester, onPick: () -> Unit) { +private fun TargetRow(label: String, free: String, removable: Boolean, focus: FocusRequester, warning: Boolean = false, onPick: () -> Unit) { val colors = MaterialTheme.colorScheme val pal = LocalPalette.current val src = remember { MutableInteractionSource() } @@ -104,6 +112,6 @@ private fun TargetRow(label: String, free: String, removable: Boolean, focus: Fo ) { Icon(if (removable) Icons.Outlined.SdCard else Icons.Outlined.Smartphone, null, tint = if (hot) pal.signal else colors.onBackground, modifier = Modifier.size(22.dp)) Text(label, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f)) - Text(free, fontSize = 12.sp, color = colors.onSurfaceVariant, maxLines = 1) + Text(free, fontSize = 12.sp, color = if (warning) colors.error else colors.onSurfaceVariant, maxLines = 1) } } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt index 1d722dd3..2494514e 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt @@ -511,10 +511,17 @@ private fun InstallWhere(item: CatalogItem) { StepTitle(title) StepList { targets.forEachIndexed { i, t -> + val needed = com.droiddeck.launcher.stores.StoreInstallRoot.estimatedBytesNeeded(item, t.root, !t.removable) + val isLow = needed > 0 && t.freeBytes in 1 until needed + val trailingText = if (isLow) { + stringResource(R.string.stores_storage_low, formatBytes(t.freeBytes), formatBytes(needed)) + } else { + stringResource(R.string.stores_target_free, formatBytes(t.freeBytes)) + } StepChoice( label = if (t.removable) t.label else internalLabel, icon = if (t.removable) androidx.compose.material.icons.Icons.Outlined.SdCard else androidx.compose.material.icons.Icons.Outlined.Smartphone, - trailing = stringResource(R.string.stores_target_free, formatBytes(t.freeBytes)), + trailing = trailingText, enabled = open, modifier = Modifier.fillMaxWidth().stepItem(1 + i).then(if (i == defaultIndex) Modifier.focusRequester(first) else Modifier), ) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3415cc58..dd47a988 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2257,6 +2257,9 @@ Install %1$s: Internal %1$s free + Not enough free space: need %1$s, only %2$s free + Not enough internal space for download cache: need %1$s, only %2$s free + Insufficient space (%1$s free, need %2$s) Store downloads Shows game download progress and keeps downloads running in the background Downloading diff --git a/app/src/test/java/com/droiddeck/launcher/stores/StoreInstallRootTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/StoreInstallRootTest.kt index 3856ed56..ae1353e7 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/StoreInstallRootTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/StoreInstallRootTest.kt @@ -59,4 +59,22 @@ class StoreInstallRootTest { assertTrue(dir.absolutePath.startsWith(app.cacheDir.absolutePath)) assertEquals("a_b_c", dir.name) } + + @Test fun estimatedBytesNeededCountsDoubleForEpicInternalStorage() { + val item = CatalogItem(Store.EPIC, "game1", "Game 1", imageUrl = null, owned = true, sizeBytes = 10_000_000L) + val internalDir = StoreInstallRoot.internalRoot(app) + val cardDir = File(app.filesDir, "card/Games") + + assertEquals(20_000_000L, StoreInstallRoot.estimatedBytesNeeded(item, internalDir, isInternal = true)) + assertEquals(10_000_000L, StoreInstallRoot.estimatedBytesNeeded(item, cardDir, isInternal = false)) + + val gogItem = CatalogItem(Store.GOG, "game2", "Game 2", imageUrl = null, owned = true, sizeBytes = 10_000_000L) + assertEquals(10_000_000L, StoreInstallRoot.estimatedBytesNeeded(gogItem, internalDir, isInternal = true)) + } + + @Test fun checkStorageReportsEnoughWhenSpaceIsUnknownOrAvailable() { + val zeroItem = CatalogItem(Store.EPIC, "game1", "Game 1", imageUrl = null, owned = true, sizeBytes = 0L) + val check = StoreInstallRoot.checkStorage(app, zeroItem, StoreInstallRoot.internalRoot(app)) + assertTrue(check.enough) + } }