mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Show storage warning and error message on insufficient download space
This commit is contained in:
1 parent
4163b31b02
commit
82b8cf1230
11 files changed
+228
-14
No files matched your search
@@ -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. */
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
) {
|
||||
|
||||
@@ -2257,6 +2257,9 @@
|
||||
<string name="stores_install_title">Install %1$s:</string>
|
||||
<string name="stores_target_internal">Internal</string>
|
||||
<string name="stores_target_free">%1$s free</string>
|
||||
<string name="stores_storage_insufficient">Not enough free space: need %1$s, only %2$s free</string>
|
||||
<string name="stores_storage_internal_cache_insufficient">Not enough internal space for download cache: need %1$s, only %2$s free</string>
|
||||
<string name="stores_storage_low">Insufficient space (%1$s free, need %2$s)</string>
|
||||
<string name="stores_notification_channel">Store downloads</string>
|
||||
<string name="stores_notification_channel_hint">Shows game download progress and keeps downloads running in the background</string>
|
||||
<string name="stores_notification_one">Downloading</string>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user