diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2b0e5ee..7a3144a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -25,17 +25,14 @@
diff --git a/app/src/main/java/eu/m724/pojavbackup/core/backup/BackupModule.kt b/app/src/main/java/eu/m724/pojavbackup/core/backup/BackupModule.kt
index 4ae04b6..bfc4b3f 100644
--- a/app/src/main/java/eu/m724/pojavbackup/core/backup/BackupModule.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/core/backup/BackupModule.kt
@@ -1,26 +1,21 @@
package eu.m724.pojavbackup.core.backup
-import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
-import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import eu.m724.pojavbackup.core.backup.Backup.BackupStatus
import java.time.Instant
import java.util.UUID
import java.util.concurrent.ThreadLocalRandom
-import kotlin.io.path.ExperimentalPathApi
@Module
@InstallIn(SingletonComponent::class)
object BackupModule {
@Provides
- fun provideBackupRepository(
- @ApplicationContext context: Context
- ): BackupRepository {
+ fun provideBackupRepository(): BackupRepository {
return object : BackupRepository {
- override suspend fun createBackup(): Backup {
+ override fun createBackup(): Backup {
val bytes = ByteArray(16)
ThreadLocalRandom.current().nextBytes(bytes)
@@ -31,8 +26,7 @@ object BackupModule {
)
}
- @OptIn(ExperimentalPathApi::class)
- override suspend fun completeBackup(backup: Backup, status: BackupStatus): Backup {
+ override fun completeBackup(backup: Backup, status: BackupStatus): Backup {
return backup.copy(
status = status
)
diff --git a/app/src/main/java/eu/m724/pojavbackup/core/backup/BackupRepository.kt b/app/src/main/java/eu/m724/pojavbackup/core/backup/BackupRepository.kt
index 764176a..499d157 100644
--- a/app/src/main/java/eu/m724/pojavbackup/core/backup/BackupRepository.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/core/backup/BackupRepository.kt
@@ -3,6 +3,6 @@ package eu.m724.pojavbackup.core.backup
import eu.m724.pojavbackup.core.backup.Backup.BackupStatus
interface BackupRepository {
- suspend fun createBackup(): Backup
- suspend fun completeBackup(backup: Backup, status: BackupStatus = BackupStatus.SUCCESS): Backup
+ fun createBackup(): Backup
+ fun completeBackup(backup: Backup, status: BackupStatus = BackupStatus.SUCCESS): Backup
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/core/backup/BackupWorker.kt b/app/src/main/java/eu/m724/pojavbackup/core/backup/BackupWorker.kt
index 196b394..aa8e28d 100644
--- a/app/src/main/java/eu/m724/pojavbackup/core/backup/BackupWorker.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/core/backup/BackupWorker.kt
@@ -1,8 +1,8 @@
package eu.m724.pojavbackup.core.backup
-import android.app.NotificationManager
import android.content.Context
import android.content.pm.ServiceInfo
+import android.net.Uri
import androidx.core.app.NotificationCompat
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
@@ -15,10 +15,12 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import eu.m724.pojavbackup.R
import eu.m724.pojavbackup.core.backup.Backup.BackupStatus
+import eu.m724.pojavbackup.core.backup.exception.BackupFailedException
+import eu.m724.pojavbackup.core.backup.exception.BackupOfWorldException
import eu.m724.pojavbackup.core.data.GameDataRepository
+import eu.m724.pojavbackup.core.data.World
import eu.m724.pojavbackup.core.datastore.SettingsRepository
import eu.m724.pojavbackup.notification.NotificationChannels
-import org.apache.commons.compress.compressors.CompressorStreamFactory
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.UUID
@@ -32,106 +34,113 @@ class BackupWorker @AssistedInject constructor(
private val backupRepository: BackupRepository,
private val gameDataRepository: GameDataRepository
) : CoroutineWorker(appContext, workerParams) {
- private val notificationManager =
- applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as
- NotificationManager
-
private val notificationId: Int =
Random.nextInt()
- private val errors = mutableListOf()
-
- private lateinit var backup: Backup
-
override suspend fun doWork(): Result {
+ val backup = backupRepository.createBackup()
+
try {
- backup = backupRepository.createBackup()
+ doBackup(backup)
- updateStatus("Initializing")
-
- val settings = settingsRepository.getSettings()
- val worldIds = settingsRepository.getIncludedWorldIds(settings.worldOrder)
-
- // TODO multiple destinations support and remove those !!
- val backupUri = settings.destinationsList.first().uri.toUri()
- val backupDirectory = DocumentFile.fromTreeUri(applicationContext, backupUri)!!
- .createDirectory(DateTimeFormatter.ISO_LOCAL_DATE.format(LocalDate.now()) + " (" + backup.id + ")")!!
-
- worldIds.forEach {
- updateStatus("Backing up world $it")
-
- val world = runCatching {
- gameDataRepository.getWorld(it)
- }
-
- if (world.isFailure) {
- val e = Exception("Error backing up world $it", world.exceptionOrNull())
- errors.add(e)
-
- e.printStackTrace()
- return@forEach
- }
-
- val compression = if (settings.improvement) CompressorStreamFactory.ZSTANDARD else CompressorStreamFactory.GZIP
- val documentFile = backupDirectory.createFile("application/zstd", "$it.tar.$compression")!!
-
- applicationContext.contentResolver.openOutputStream(documentFile.uri)!!.use {
- PathTools.compressDocumentFileDirectory(
- contentResolver = applicationContext.contentResolver,
- source = world.getOrThrow().documentFile!!,
- target = it,
- compression = compression,
- inflate = settings.improvement
- )
- }
- }
- } catch (e: Exception) {
- val e = Exception("Error backing up", e)
- errors.add(e)
-
- e.printStackTrace()
- }
-
- if (errors.isEmpty()) {
backupRepository.completeBackup(backup, BackupStatus.SUCCESS)
+ return Result.success(statusData(backup.id, "Backup completed successfully"))
+ } catch (cause: Throwable) {
+ val e = throw BackupFailedException(backup, cause)
+ // TODO raise this error
- return Result.success(statusData("Backup completed successfully"))
- } else {
backupRepository.completeBackup(backup, BackupStatus.FAILURE)
-
- // TODO notify and tell error
-
- return Result.failure(statusData("Backup failed"))
+ return Result.failure(statusData(backup.id, "Backup failed"))
}
}
- fun statusData(status: String): Data {
+ private suspend fun doBackup(backup: Backup) {
+ val settings = settingsRepository.getSettings()
+ val worldIds = settingsRepository.getIncludedWorldIds(settings.worldOrder)
+
+ // TODO multiple destinations support
+ val destinationUri = settings.destinationsList.first().uri.toUri()
+ val backupDirectory = createBackupDirectory(backup.id, destinationUri)
+
+ val documentFileCompressor = DocumentFileCompressor(
+ openInputStream = {
+ applicationContext.contentResolver.openInputStream(it.uri)!!
+ }
+ )
+
+ worldIds.forEach {
+ updateStatus(backup.id, it)
+
+ try {
+ val world = gameDataRepository.getWorld(it)
+ doBackupWorld(world, documentFileCompressor, backupDirectory)
+ } catch (e: Throwable) {
+ // TODO maybe continue with other worlds
+ throw BackupOfWorldException(backup, e)
+ }
+
+ }
+ }
+
+ private fun doBackupWorld(
+ world: World,
+ documentFileCompressor: DocumentFileCompressor,
+ backupDirectory: DocumentFile
+ ) {
+ val documentFile = backupDirectory.createFile("application/zstd", "${world.id}.tar.gz")
+
+ if (documentFile == null) {
+ // TODO generic Exception?
+ throw Exception("Failed to create file: \"${world.id}.tar.gz\" in: ${backupDirectory.uri}")
+ }
+
+ val outputStream = applicationContext.contentResolver.openOutputStream(documentFile.uri)
+
+ if (outputStream == null) {
+ // TODO generic Exception?
+ throw Exception("Failed to open for writing: ${documentFile.uri}")
+ }
+
+ outputStream.use {
+ documentFileCompressor.compressToOutputStream(
+ source = world.documentFile!!,
+ target = it
+ )
+ }
+ }
+
+ private fun createBackupDirectory(
+ backupId: UUID,
+ destinationUri: Uri
+ ): DocumentFile {
+ val destinationRoot = DocumentFile.fromTreeUri(applicationContext, destinationUri)!! // would return null only <=A5
+
+ val destination = destinationRoot.createDirectory(DateTimeFormatter.ISO_LOCAL_DATE.format(LocalDate.now()) + " (" + backupId + ")")
+
+ if (destination == null) {
+ // TODO generic Exception?
+ throw Exception("Failed to create backup destination directory")
+ }
+
+ return destination
+ }
+
+ private fun statusData(uuid: UUID, status: String): Data {
return Data.Builder()
- .putUuid("uuid", backup.id)
+ .putUuid("uuid", uuid)
.putString("status", status)
.build()
}
- suspend fun updateStatus(status: String) {
+ private suspend fun updateStatus(uuid: UUID, status: String) {
setForeground(createForegroundInfo(status))
- val data = statusData(status)
+ val data = statusData(uuid, status)
setProgress(data)
}
- fun finish(status: String, success: Boolean) {
- val data = statusData(status)
-
- if (success) {
- Result.success(data)
- } else {
- Result.failure(data)
- }
- }
-
-
- fun Data.Builder.putUuid(key: String, value: UUID): Data.Builder {
+ private fun Data.Builder.putUuid(key: String, value: UUID): Data.Builder {
return this.putLongArray(key, longArrayOf(value.mostSignificantBits, value.leastSignificantBits))
}
diff --git a/app/src/main/java/eu/m724/pojavbackup/core/backup/DocumentFileCompressor.kt b/app/src/main/java/eu/m724/pojavbackup/core/backup/DocumentFileCompressor.kt
new file mode 100644
index 0000000..462197b
--- /dev/null
+++ b/app/src/main/java/eu/m724/pojavbackup/core/backup/DocumentFileCompressor.kt
@@ -0,0 +1,54 @@
+package eu.m724.pojavbackup.core.backup
+
+import androidx.documentfile.provider.DocumentFile
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry
+import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream
+import org.apache.commons.compress.compressors.CompressorStreamFactory
+import java.io.InputStream
+import java.io.OutputStream
+
+class DocumentFileCompressor(
+ private val openInputStream: (DocumentFile) -> InputStream,
+) {
+ fun compressToOutputStream(
+ source: DocumentFile,
+ target: OutputStream,
+ compressionAlgorithm: String = CompressorStreamFactory.GZIP
+ ) {
+ CompressorStreamFactory().createCompressorOutputStream(compressionAlgorithm, target).use { compressorOutputStream ->
+ TarArchiveOutputStream(compressorOutputStream).use { tarArchiveOutputStream ->
+ writeTarEntry(tarArchiveOutputStream, source)
+
+ tarArchiveOutputStream.finish()
+ }
+ }
+ }
+
+ private fun writeTarEntry(
+ tarArchiveOutputStream: TarArchiveOutputStream,
+ file: DocumentFile,
+ prefix: String = "",
+ ) {
+ val entryName = prefix + file.name + if (file.isDirectory) "/" else ""
+ val entry = TarArchiveEntry(entryName)
+
+ entry.setModTime(file.lastModified())
+ entry.size = file.length() // for directory this doesn't matter
+
+ tarArchiveOutputStream.putArchiveEntry(entry)
+
+ if (!file.isDirectory) {
+ openInputStream(file).use { inputStream ->
+ inputStream.copyTo(tarArchiveOutputStream)
+ }
+ }
+
+ tarArchiveOutputStream.closeArchiveEntry()
+
+ if (file.isDirectory) {
+ file.listFiles().forEach { child ->
+ writeTarEntry(tarArchiveOutputStream, child, entryName)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/core/backup/McaUtils.kt b/app/src/main/java/eu/m724/pojavbackup/core/backup/McaUtils.kt
deleted file mode 100644
index 4348962..0000000
--- a/app/src/main/java/eu/m724/pojavbackup/core/backup/McaUtils.kt
+++ /dev/null
@@ -1,349 +0,0 @@
-package eu.m724.pojavbackup.core.backup
-
-import java.io.BufferedInputStream
-import java.io.BufferedOutputStream
-import java.io.ByteArrayInputStream
-import java.io.DataOutputStream
-import java.io.IOException
-import java.io.InputStream
-import java.io.OutputStream
-import java.nio.ByteBuffer
-import java.util.zip.InflaterInputStream
-import java.util.zip.ZipException
-import kotlin.math.ceil
-
-// --- Constants ---
-const val SECTOR_BYTES = 4096
-const val CHUNK_HEADER_SIZE = 5 // 4 bytes length, 1 byte compression type
-const val REGION_HEADER_TABLES_SIZE = 2 * SECTOR_BYTES // Location table + Timestamp table
-const val CHUNKS_PER_REGION_SIDE = 32
-const val CHUNKS_PER_REGION = CHUNKS_PER_REGION_SIDE * CHUNKS_PER_REGION_SIDE // 1024
-const val MAX_CHUNK_SECTORS = 255 // Max value for the 1-byte size field
-
-// --- Data Classes ---
-
-/** Represents an entry in the MCA location table. */
-data class LocationEntry(val offsetSectors: Int, val sizeSectors: Int) {
- val byteOffset: Long = offsetSectors.toLong() * SECTOR_BYTES
- val paddedByteSize: Int = sizeSectors * SECTOR_BYTES
- val exists: Boolean = offsetSectors > 0 && sizeSectors > 0
-
- companion object {
- /** Parses the 4-byte integer from the location table. */
- fun fromInt(value: Int): LocationEntry {
- val offset = value ushr 8
- val size = value and 0xFF
- // Basic validation: offset shouldn't point inside header unless 0
- if (offset > 0 && offset < (REGION_HEADER_TABLES_SIZE / SECTOR_BYTES)) {
- println("Warning: LocationEntry points inside header (offset sectors: $offset). Treating as non-existent.")
- return LocationEntry(0, 0)
- }
- return LocationEntry(offset, size)
- }
- }
-
- /** Converts back to the 4-byte integer format for writing. */
- fun toInt(): Int {
- if (offsetSectors >= (1 shl 24) || offsetSectors < 0) {
- println("Warning: Attempting to create LocationEntry integer with invalid offset: $offsetSectors sectors. Clamping to 0.")
- return 0 // Cannot represent this offset
- }
- if (sizeSectors >= (1 shl 8) || sizeSectors < 0) {
- println("Warning: Attempting to create LocationEntry integer with invalid size: $sizeSectors sectors. Clamping to 0.")
- return 0 // Cannot represent this size
- }
- return (offsetSectors shl 8) or (sizeSectors and 0xFF)
- }
-}
-
-/** Stores calculated size info after the first pass. */
-private data class ProcessedChunkSizeInfo(
- val originalIndex: Int,
- val newPaddedSize: Int,
- val newSizeSectors: Int
-)
-
-/**
- * Converts an MCA region file stream, decompressing ZLIB chunks to uncompressed.
- * Reads from InputStream, writes to OutputStream.
- *
- * WARNING: This implementation buffers the entire chunk data region of the
- * input stream into memory to simulate random access. It WILL cause
- * OutOfMemoryError if the input stream represents a file too large to fit
- * the chunk data portion in available RAM. This is an inherent limitation
- * when working with non-seekable streams and the MCA format.
- *
- * @param inputStream The input stream containing the original MCA file data.
- * @param outputStream The output stream where the modified MCA file data will be written.
- * @throws IOException If an I/O error occurs during reading or writing.
- * @throws OutOfMemoryError If the input stream's chunk data region is too large for memory.
- * @throws ZipException If ZLIB decompression fails for a chunk.
- */
-fun convertMcaToUncompressedStream(inputStream: InputStream, outputStream: OutputStream) {
-
- // Use buffered streams for efficiency
- val bis = inputStream as? BufferedInputStream ?: BufferedInputStream(inputStream)
- val bos = outputStream as? BufferedOutputStream ?: BufferedOutputStream(outputStream, 65536) // Buffer output
- val dos = DataOutputStream(bos)
-
- // --- 1. Read Header Tables ---
- println("Reading header tables...")
- val headerBytes = ByteArray(REGION_HEADER_TABLES_SIZE)
- val headerBytesRead = bis.readNBytes(headerBytes, 0, REGION_HEADER_TABLES_SIZE)
- if (headerBytesRead < REGION_HEADER_TABLES_SIZE) {
- throw IOException("Input stream too short to contain MCA header ($headerBytesRead bytes read)")
- }
- val headerBuffer = ByteBuffer.wrap(headerBytes)
-
- val originalLocationEntries = Array(CHUNKS_PER_REGION) { LocationEntry(0, 0) }
- val originalTimestamps = IntArray(CHUNKS_PER_REGION)
-
- for (i in 0 until CHUNKS_PER_REGION) {
- originalLocationEntries[i] = LocationEntry.fromInt(headerBuffer.getInt())
- }
- for (i in 0 until CHUNKS_PER_REGION) {
- originalTimestamps[i] = headerBuffer.getInt()
- }
- println("Header tables read.")
-
- // --- 2. Buffer Remaining Input Stream Data (Chunk Data Region) ---
- // !!! THIS IS THE MEMORY-INTENSIVE STEP !!!
- println("Buffering input stream chunk data... (Potential OOM)")
- val inputChunkDataBytes: ByteArray = try {
- bis.readAllBytes() // Reads everything *after* the header
- } catch (oom: OutOfMemoryError) {
- println("FATAL: OutOfMemoryError while buffering input stream data.")
- println("The input MCA data is too large to fit in memory using this stream-based method.")
- println("Consider using a file-based approach if possible.")
- throw oom // Re-throw the error
- }
- val inputChunkDataSize = inputChunkDataBytes.size
- println("Input chunk data buffered (${inputChunkDataSize} bytes).")
-
- // --- 3. First Pass: Calculate New Sizes ---
- println("Pass 1: Calculating final chunk sizes...")
- val processedChunkInfos = mutableListOf()
- var processedCount = 0
-
- for (i in 0 until CHUNKS_PER_REGION) {
- val entry = originalLocationEntries[i]
- if (!entry.exists) continue
-
- // Calculate offset relative to the buffered chunk data
- val chunkOffsetInDataRegion = entry.byteOffset - REGION_HEADER_TABLES_SIZE
- if (chunkOffsetInDataRegion < 0 || chunkOffsetInDataRegion + CHUNK_HEADER_SIZE > inputChunkDataSize) {
- println("Warning [Pass 1]: Chunk $i location invalid or header out of buffered bounds. Offset: ${entry.byteOffset}. Skipping.")
- originalLocationEntries[i] = LocationEntry(0, 0) // Mark as non-existent
- continue
- }
-
- var actualLength: Int
- var compressionType: Byte
- var payload: ByteArray
-
- try {
- // Read header directly from the byte array buffer
- val chunkHeaderBuffer = ByteBuffer.wrap(inputChunkDataBytes, chunkOffsetInDataRegion.toInt(), CHUNK_HEADER_SIZE)
- actualLength = chunkHeaderBuffer.getInt()
- compressionType = chunkHeaderBuffer.get()
-
- // Validate header data
- if (actualLength <= 0 || actualLength > entry.paddedByteSize - 4) {
- throw IOException("Invalid actual length: $actualLength (padded: ${entry.paddedByteSize})")
- }
- val payloadSize = actualLength - 1
- val payloadOffsetInDataRegion = chunkOffsetInDataRegion + CHUNK_HEADER_SIZE
- if (payloadOffsetInDataRegion + payloadSize > inputChunkDataSize) {
- throw IOException("Chunk payload extends beyond buffered data bounds (Offset: ${entry.byteOffset}, Declared Size: $actualLength)")
- }
-
- // Extract payload reference (no copy needed yet for size calculation if compressed)
- // We only need the actual payload bytes if we decompress
- payload = if (compressionType == 2.toByte()) {
- // Need to read the payload to decompress it for size calculation
- inputChunkDataBytes.copyOfRange(payloadOffsetInDataRegion.toInt(), (payloadOffsetInDataRegion + payloadSize).toInt())
- } else {
- // For non-ZLIB, we only need the size, not the data itself yet
- ByteArray(payloadSize) // Dummy array of correct size
- }
-
- } catch (e: Exception) { // Catch IndexOutOfBounds too
- println("Warning [Pass 1]: Error reading header/payload for chunk $i at offset ${entry.byteOffset}. Skipping. Error: ${e.message}")
- originalLocationEntries[i] = LocationEntry(0, 0) // Mark as non-existent
- continue
- }
-
- // --- Determine New Payload Size ---
- var newPayloadSize = payload.size // Start with original size
-
- if (compressionType == 2.toByte()) { // ZLIB compressed
- try {
- // Decompress just to get the size
- val bais = ByteArrayInputStream(payload) // Use the actual payload read earlier
- val iis = InflaterInputStream(bais)
- // Read fully to determine decompressed size, discard the data
- val decompressedBytes = iis.readAllBytes()
- newPayloadSize = decompressedBytes.size
- // newCompressionType will be 3 in the second pass
- } catch (e: ZipException) {
- println("Warning [Pass 1]: Failed to decompress ZLIB chunk $i to calculate size. Using original size. Error: ${e.message}")
- // Keep original size, type will remain 2 in second pass
- newPayloadSize = payload.size
- } catch (e: IOException) {
- println("Warning [Pass 1]: IO error during ZLIB decompression for size calculation chunk $i. Using original size. Error: ${e.message}")
- newPayloadSize = payload.size
- }
- }
- // else: newPayloadSize remains payload.size
-
- // --- Calculate Final Sector Size ---
- val newActualDataSize = CHUNK_HEADER_SIZE + newPayloadSize
- val newSizeSectors = ceil(newActualDataSize.toDouble() / SECTOR_BYTES).toInt()
-
- if (newSizeSectors > MAX_CHUNK_SECTORS) {
- println("Warning [Pass 1]: Processed chunk $i would be too large ($newSizeSectors sectors > $MAX_CHUNK_SECTORS). Skipping chunk.")
- originalLocationEntries[i] = LocationEntry(0, 0) // Mark as non-existent
- continue
- }
-
- val newPaddedSize = newSizeSectors * SECTOR_BYTES
-
- processedChunkInfos.add(
- ProcessedChunkSizeInfo(
- originalIndex = i,
- newPaddedSize = newPaddedSize,
- newSizeSectors = newSizeSectors
- )
- )
- processedCount++
- if (processedCount % 100 == 0) print(".")
-
- } // End Pass 1 loop
- println("\nPass 1 complete. Calculated sizes for $processedCount chunks.")
-
-
- // --- 4. Calculate New Location Table ---
- val newLocationInts = IntArray(CHUNKS_PER_REGION) { 0 }
- var currentOffsetBytes = REGION_HEADER_TABLES_SIZE.toLong()
- // Sort infos by original index to ensure correct offset calculation if needed,
- // though iterating 0..1023 and looking up is safer.
- // processedChunkInfos.sortBy { it.originalIndex } // Not strictly necessary if we iterate below
-
- // Create a map for quick lookup during offset calculation
- val infoMap = processedChunkInfos.associateBy { it.originalIndex }
-
- for (i in 0 until CHUNKS_PER_REGION) {
- val info = infoMap[i]
- if (info != null) {
- val offsetSectors = (currentOffsetBytes / SECTOR_BYTES).toInt()
- val newEntry = LocationEntry(offsetSectors, info.newSizeSectors)
- newLocationInts[i] = newEntry.toInt()
- currentOffsetBytes += info.newPaddedSize
- } else {
- // Ensure non-existent chunks (or skipped ones) have a 0 entry
- newLocationInts[i] = 0
- }
- }
-
-
- // --- 5. Write New Header Tables to Output ---
- println("Writing new header tables...")
- for (locationValue in newLocationInts) {
- dos.writeInt(locationValue)
- }
- for (timestampValue in originalTimestamps) {
- dos.writeInt(timestampValue)
- }
- dos.flush() // Ensure headers are written before chunk data
-
- // --- 6. Second Pass: Process Again and Write Chunk Data ---
- println("Pass 2: Processing and writing chunk data...")
- processedCount = 0
- for (i in 0 until CHUNKS_PER_REGION) {
- // Use the potentially modified originalLocationEntries (some might be marked non-existent now)
- val entry = originalLocationEntries[i]
- // Also double-check if we calculated info for it (it might have been skipped in pass 1)
- val sizeInfo = infoMap[i]
-
- if (!entry.exists || sizeInfo == null) continue // Skip non-existent or skipped chunks
-
- // Offsets are the same as in Pass 1
- val chunkOffsetInDataRegion = entry.byteOffset - REGION_HEADER_TABLES_SIZE
- // No need to re-validate bounds, done in Pass 1
-
- var actualLength: Int
- var compressionType: Byte
- var payload: ByteArray
-
- try {
- // Read header again
- val chunkHeaderBuffer = ByteBuffer.wrap(inputChunkDataBytes, chunkOffsetInDataRegion.toInt(), CHUNK_HEADER_SIZE)
- actualLength = chunkHeaderBuffer.getInt()
- compressionType = chunkHeaderBuffer.get()
- val payloadSize = actualLength - 1
- val payloadOffsetInDataRegion = chunkOffsetInDataRegion + CHUNK_HEADER_SIZE
-
- // Read payload again - this time we need the actual data
- payload = inputChunkDataBytes.copyOfRange(payloadOffsetInDataRegion.toInt(), (payloadOffsetInDataRegion + payloadSize).toInt())
-
- } catch (e: Exception) {
- // Should not happen if Pass 1 succeeded, but good practice
- println("Error [Pass 2]: Unexpected error reading chunk $i data at offset ${entry.byteOffset}. Skipping write. Error: ${e.message}")
- // This will lead to a gap/corruption in the output file!
- continue
- }
-
- // --- Decompress if necessary (again) ---
- var newPayload = payload
- var newCompressionType = compressionType
-
- if (compressionType == 2.toByte()) { // ZLIB compressed
- try {
- val bais = ByteArrayInputStream(payload)
- val iis = InflaterInputStream(bais)
- newPayload = iis.readAllBytes() // Decompress fully
- newCompressionType = 3.toByte() // Mark as uncompressed
- } catch (e: Exception) { // Catch ZipException or IOException
- println("Warning [Pass 2]: Failed to decompress ZLIB chunk $i. Writing as-is. Error: ${e.message}")
- newPayload = payload // Keep original
- newCompressionType = compressionType
- }
- }
- // else: Keep original payload and type
-
- // --- Prepare Data for Writing ---
- val newActualDataSize = CHUNK_HEADER_SIZE + newPayload.size
- // Use calculated sizes from Pass 1
- val newPaddedSize = sizeInfo.newPaddedSize
- val paddingSize = newPaddedSize - newActualDataSize
-
- // --- Write Processed Chunk to Output Stream ---
- try {
- dos.writeInt(newPayload.size + 1) // New actual length
- dos.writeByte(newCompressionType.toInt()) // New compression type
- dos.flush() // Flush DataOutputStream buffer before writing byte array directly
-
- bos.write(newPayload) // Write payload bytes directly to underlying BufferedOutputStream
- if (paddingSize > 0) {
- bos.write(ByteArray(paddingSize)) // Write padding zeros
- }
- } catch (e: IOException) {
- println("Error [Pass 2]: Failed to write processed chunk $i to output stream. Aborting? Error: ${e.message}")
- // Depending on requirements, might want to re-throw or try to continue
- throw e // Rethrow for now
- }
-
- processedCount++
- if (processedCount % 100 == 0) print(".")
-
- } // End Pass 2 loop
- println("\nPass 2 complete. Wrote $processedCount chunks.")
-
- // --- 7. Final Flush and Cleanup ---
- println("Flushing output stream...")
- bos.flush() // Ensure all buffered data is written to the underlying outputStream
- println("Conversion complete.")
-
- // Note: We don't close the input or output streams here.
- // The caller who provided the streams is responsible for closing them.
-}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/core/backup/PathTools.kt b/app/src/main/java/eu/m724/pojavbackup/core/backup/PathTools.kt
deleted file mode 100644
index 16928f1..0000000
--- a/app/src/main/java/eu/m724/pojavbackup/core/backup/PathTools.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-package eu.m724.pojavbackup.core.backup
-
-import android.content.ContentResolver
-import androidx.documentfile.provider.DocumentFile
-import org.apache.commons.compress.archivers.tar.TarArchiveEntry
-import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream
-import org.apache.commons.compress.compressors.CompressorStreamFactory
-import java.io.ByteArrayOutputStream
-import java.io.OutputStream
-
-class PathTools {
- companion object {
- fun compressDocumentFileDirectory(
- contentResolver: ContentResolver,
- source: DocumentFile,
- target: OutputStream,
- compression: String,
- inflate: Boolean = false,
- ) {
- target.use {
- CompressorStreamFactory().createCompressorOutputStream(compression, it).use {
- TarArchiveOutputStream(it).use { outputStream ->
- compressInner(contentResolver, source, outputStream, "", inflate)
- outputStream.finish()
- }
- }
- }
- }
-
- private fun compressInner(
- contentResolver: ContentResolver,
- source: DocumentFile,
- archiveOutputStream: TarArchiveOutputStream,
- prefix: String = "",
- inflate: Boolean = false,
- ) {
- source.listFiles().forEach {
- if (!it.isDirectory) {
- val entry = TarArchiveEntry(prefix + it.name)
- entry.setModTime(it.lastModified())
-
- val inflate = inflate and it.name!!.endsWith(".mca")
-
- if (inflate) {
- contentResolver.openInputStream(it.uri)!!.use { inputStream ->
- ByteArrayOutputStream().use { outputStream ->
- convertMcaToUncompressedStream(inputStream, outputStream)
-
- entry.size = outputStream.size().toLong()
-
- archiveOutputStream.putArchiveEntry(entry)
- outputStream.writeTo(archiveOutputStream)
- }
- }
- } else {
- entry.size = it.length()
-
- archiveOutputStream.putArchiveEntry(entry)
-
- contentResolver.openInputStream(it.uri)!!.use { inputStream ->
- inputStream.copyTo(archiveOutputStream)
- }
- }
-
- archiveOutputStream.closeArchiveEntry()
- } else {
- val entry = TarArchiveEntry(prefix + it.name + "/")
- entry.setModTime(it.lastModified())
- archiveOutputStream.putArchiveEntry(entry)
- archiveOutputStream.closeArchiveEntry() // Close directory entry immediately (no content)
-
- compressInner(contentResolver, it, archiveOutputStream, prefix + it.name + "/", inflate)
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/core/backup/exception/BackupException.kt b/app/src/main/java/eu/m724/pojavbackup/core/backup/exception/BackupException.kt
new file mode 100644
index 0000000..7f66a98
--- /dev/null
+++ b/app/src/main/java/eu/m724/pojavbackup/core/backup/exception/BackupException.kt
@@ -0,0 +1,8 @@
+package eu.m724.pojavbackup.core.backup.exception
+
+import eu.m724.pojavbackup.core.backup.Backup
+
+open class BackupException(
+ backup: Backup,
+ cause: Throwable
+) : Exception("Exception backing up (backup ID: ${backup.id})", cause)
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/core/backup/exception/BackupFailedException.kt b/app/src/main/java/eu/m724/pojavbackup/core/backup/exception/BackupFailedException.kt
new file mode 100644
index 0000000..2566ae7
--- /dev/null
+++ b/app/src/main/java/eu/m724/pojavbackup/core/backup/exception/BackupFailedException.kt
@@ -0,0 +1,9 @@
+package eu.m724.pojavbackup.core.backup.exception
+
+import eu.m724.pojavbackup.core.backup.Backup
+
+class BackupFailedException(
+ backup: Backup,
+ cause: Throwable
+) : BackupException(backup, cause) {
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/core/backup/exception/BackupOfWorldException.kt b/app/src/main/java/eu/m724/pojavbackup/core/backup/exception/BackupOfWorldException.kt
new file mode 100644
index 0000000..57a13fa
--- /dev/null
+++ b/app/src/main/java/eu/m724/pojavbackup/core/backup/exception/BackupOfWorldException.kt
@@ -0,0 +1,8 @@
+package eu.m724.pojavbackup.core.backup.exception
+
+import eu.m724.pojavbackup.core.backup.Backup
+
+class BackupOfWorldException(
+ backup: Backup,
+ cause: Throwable
+) : BackupException(backup, cause)
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/core/datastore/SettingsRepository.kt b/app/src/main/java/eu/m724/pojavbackup/core/datastore/SettingsRepository.kt
index 5e5fcae..d263523 100644
--- a/app/src/main/java/eu/m724/pojavbackup/core/datastore/SettingsRepository.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/core/datastore/SettingsRepository.kt
@@ -23,22 +23,6 @@ class SettingsRepository @Inject constructor(
return dataStore.data.first()
}
- suspend fun updateSettings(
- improvement: Boolean? = null
- ) {
- dataStore.updateData {
- val builder = it.toBuilder()
-
- if (improvement != null) {
- builder.setImprovement(improvement)
- }
-
- // add here
-
- builder.build()
- }
- }
-
/* Worlds */
suspend fun getIncludedWorldIds(): List {
diff --git a/app/src/main/java/eu/m724/pojavbackup/home/HomeActivity.kt b/app/src/main/java/eu/m724/pojavbackup/home/HomeActivity.kt
index f38fa4d..9cae442 100644
--- a/app/src/main/java/eu/m724/pojavbackup/home/HomeActivity.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/home/HomeActivity.kt
@@ -32,6 +32,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -119,7 +120,7 @@ fun HomeScaffold(
topBar = {
TopAppBar(
title = {
- Text("PojavBackup")
+ Text(stringResource(R.string.app_name))
}
)
},
@@ -127,13 +128,13 @@ fun HomeScaffold(
NavigationBar {
ScreenNavigationBarItem(
navController = navController,
- label = "Dashboard",
+ label = stringResource(R.string.header_dashboard),
route = HomeScreen.Dashboard,
iconResourceId = R.drawable.baseline_home_24
)
ScreenNavigationBarItem(
navController = navController,
- label = "History",
+ label = stringResource(R.string.header_history),
route = HomeScreen.History,
iconResourceId = R.drawable.baseline_history_24
)
diff --git a/app/src/main/java/eu/m724/pojavbackup/home/screen/dashboard/DashboardScreen.kt b/app/src/main/java/eu/m724/pojavbackup/home/screen/dashboard/DashboardScreen.kt
index 660cc65..1cd7304 100644
--- a/app/src/main/java/eu/m724/pojavbackup/home/screen/dashboard/DashboardScreen.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/home/screen/dashboard/DashboardScreen.kt
@@ -19,6 +19,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -45,21 +46,21 @@ fun DashboardScreen(
maxItemsInEachRow = 3
) {
DashboardCard(
- title = "Worlds included",
+ title = stringResource(R.string.dashboard_card_worlds),
value = settings.worldOrder.separatorIndex,
iconResourceId = R.drawable.baseline_mosque_24,
onClick = onWorldsIncludedClick
)
DashboardCard(
- title = "Health",
- value = "Good",
+ title = stringResource(R.string.dashboard_card_health),
+ value = "Good", // TODO
iconResourceId = R.drawable.baseline_heart_broken_24
)
DashboardCard(
- title = "Last backup",
- value = "1d ago",
+ title = stringResource(R.string.dashboard_card_last_backup),
+ value = "1d ago", // TODO
iconResourceId = R.drawable.baseline_access_time_filled_24,
onClick = {
navController.navigate(HomeScreen.History)
@@ -119,7 +120,7 @@ fun DashboardCard(
if (onClick != null) {
Icon(
painter = painterResource(R.drawable.baseline_arrow_forward_ios_24),
- contentDescription = "Go to $title",
+ contentDescription = stringResource(R.string.dashboard_card_click, title),
modifier = Modifier.padding(end = 2.dp).size(12.dp),
tint = MaterialTheme.colorScheme.secondary
)
diff --git a/app/src/main/java/eu/m724/pojavbackup/home/screen/history/HistoryScreen.kt b/app/src/main/java/eu/m724/pojavbackup/home/screen/history/HistoryScreen.kt
index e8fbbd0..71c53bc 100644
--- a/app/src/main/java/eu/m724/pojavbackup/home/screen/history/HistoryScreen.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/home/screen/history/HistoryScreen.kt
@@ -123,7 +123,7 @@ fun BackupCard(
modifier = Modifier.width(5.dp)
)
Text(
- text = "$formattedTimestamp", // Use formatted timestamp
+ text = formattedTimestamp,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
diff --git a/app/src/main/java/eu/m724/pojavbackup/settings/SettingsActivity.kt b/app/src/main/java/eu/m724/pojavbackup/settings/SettingsActivity.kt
index 0851a07..81f5cd1 100644
--- a/app/src/main/java/eu/m724/pojavbackup/settings/SettingsActivity.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/settings/SettingsActivity.kt
@@ -26,6 +26,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.compose.NavHost
@@ -69,7 +70,7 @@ class SettingsActivity : ComponentActivity() {
topBar = {
TopAppBar(
title = {
- Text("Settings")
+ Text(stringResource(R.string.header_settings))
},
navigationIcon = {
IconButton(
@@ -79,7 +80,7 @@ class SettingsActivity : ComponentActivity() {
) {
Icon(
painter = painterResource(R.drawable.baseline_arrow_back_24),
- contentDescription = "Exit Settings"
+ contentDescription = stringResource(R.string.settings_exit)
)
}
}
@@ -89,19 +90,19 @@ class SettingsActivity : ComponentActivity() {
NavigationBar {
ScreenNavigationBarItem(
navController = navController,
- label = "Options",
+ label = stringResource(R.string.settings_options),
route = SettingsScreen.Options,
iconResourceId = R.drawable.baseline_settings_24
)
ScreenNavigationBarItem(
navController = navController,
- label = "Content",
+ label = stringResource(R.string.settings_content),
route = SettingsScreen.Content,
iconResourceId = R.drawable.baseline_folder_copy_24
)
ScreenNavigationBarItem(
navController = navController,
- label = "Destination",
+ label = stringResource(R.string.settings_destination),
route = SettingsScreen.Destination,
iconResourceId = R.drawable.baseline_cloud_24
)
diff --git a/app/src/main/java/eu/m724/pojavbackup/settings/screen/content/ContentScreen.kt b/app/src/main/java/eu/m724/pojavbackup/settings/screen/content/ContentScreen.kt
index 1d84839..d5b6d77 100644
--- a/app/src/main/java/eu/m724/pojavbackup/settings/screen/content/ContentScreen.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/settings/screen/content/ContentScreen.kt
@@ -28,9 +28,8 @@ import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -62,7 +61,7 @@ fun ContentScreen(
} else {
if (uiState.worlds.size <= 1) { // separator
Text(
- text = "No worlds available!",
+ text = stringResource(R.string.worlds_none),
modifier = Modifier.padding(top = 50.dp)
)
} else {
@@ -91,7 +90,7 @@ fun ContentScreen(
)
} else {
Text(
- text = "↑ Worlds above this line will be backed up ↑",
+ text = "↑ " + stringResource(R.string.worlds_separator) + " ↑",
modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp),
textAlign = TextAlign.Center
)
@@ -110,19 +109,14 @@ fun ContentScreen(
*
* @param modifier Optional Modifier for the Card.
* @param bitmap The Bitmap for the icon to display. If null, uses a pack.png-like default icon.
- * @param iconSize The size for the square icon (width and height).
* @param id The ID text to display.
* @param displayName The display name text.
* @param lastPlayed The ZonedDateTime timestamp to display, formatted by locale.
- * @param elevation The elevation of the card.
- * @param internalPadding Padding inside the card, around the content.
- * @param spacingBetweenIconAndText Space between the icon and the text column.
*/
@Composable
fun WorldInfoCard(
modifier: Modifier = Modifier,
bitmap: Bitmap?,
- iconSize: Dp = 64.dp, // Control icon size here
id: String,
displayName: String,
lastPlayed: ZonedDateTime
@@ -160,7 +154,7 @@ fun WorldInfoCard(
painter = painter, // Use the determined painter
contentDescription = "world icon", // Hardcoded content description
modifier = Modifier
- .size(iconSize)
+ .size(64.dp)
.align(Alignment.CenterVertically)
.clip(CardDefaults.shape), // TODO match corner radius
contentScale = ContentScale.Crop // Crop is usually best for fixed aspect ratio
@@ -178,26 +172,11 @@ fun WorldInfoCard(
)
Text(
- text = "Last played $formattedTimestamp", // Use formatted timestamp
+ text = stringResource(R.string.worlds_last_played, formattedTimestamp),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
-}
-
-@Preview(showBackground = true, name = "Card with Default Icon")
-@Composable
-fun WorldInfoCardPreviewDefaultIcon() {
- MaterialTheme {
- Column(modifier = Modifier.padding(16.dp)) {
- WorldInfoCard(
- bitmap = null, // Test the default icon case
- id = "world-001",
- displayName = "Earth",
- lastPlayed = ZonedDateTime.now().minusDays(1)
- )
- }
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/settings/screen/destination/DestinationScreen.kt b/app/src/main/java/eu/m724/pojavbackup/settings/screen/destination/DestinationScreen.kt
index d459ce8..c8dd8e7 100644
--- a/app/src/main/java/eu/m724/pojavbackup/settings/screen/destination/DestinationScreen.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/settings/screen/destination/DestinationScreen.kt
@@ -18,6 +18,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -38,7 +39,9 @@ fun DestinationScreen(
horizontalAlignment = Alignment.CenterHorizontally
) {
if (destinations.isEmpty()) {
- Text("There are no destinations.")
+ Text(
+ text = stringResource(R.string.destination_none)
+ )
} else {
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally
@@ -74,7 +77,7 @@ fun DestinationScreen(
)
Text(
- text = "Add destination"
+ text = stringResource(R.string.destination_add)
)
}
}
diff --git a/app/src/main/java/eu/m724/pojavbackup/settings/screen/options/OptionsScreen.kt b/app/src/main/java/eu/m724/pojavbackup/settings/screen/options/OptionsScreen.kt
index 6d0eb2d..bb2749e 100644
--- a/app/src/main/java/eu/m724/pojavbackup/settings/screen/options/OptionsScreen.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/settings/screen/options/OptionsScreen.kt
@@ -2,18 +2,18 @@ package eu.m724.pojavbackup.settings.screen.options
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
-import androidx.compose.material3.Checkbox
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
+import eu.m724.pojavbackup.R
@Composable
fun OptionsScreen(
@@ -22,17 +22,6 @@ fun OptionsScreen(
val viewModel: OptionsScreenViewModel = hiltViewModel()
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
- Column {
- Toggle(
- title = "Optimal mode",
- description = "Uses less space, but requires special treatment to restore",
- checked = uiState.improvement,
- onCheckedChange = {
- viewModel.setOptions(improvement = it)
- }
- )
- }
-
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
@@ -43,30 +32,12 @@ fun OptionsScreen(
viewModel.backupNow()
}
) {
- Text("Backup now")
+ Text(
+ text = stringResource(R.string.button_backup_now)
+ )
}
Text(uiState.backupStatusHeader)
Text(uiState.backupStatusDetails)
}
-}
-
-@Composable
-fun Toggle(
- title: String,
- description: String,
- checked: Boolean,
- onCheckedChange: (Boolean) -> Unit
-) {
- Row {
- Checkbox(
- checked = checked,
- onCheckedChange = onCheckedChange
- )
-
- Column {
- Text(title)
- Text(description)
- }
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/settings/screen/options/OptionsScreenUiState.kt b/app/src/main/java/eu/m724/pojavbackup/settings/screen/options/OptionsScreenUiState.kt
index 0573a3b..8ca699b 100644
--- a/app/src/main/java/eu/m724/pojavbackup/settings/screen/options/OptionsScreenUiState.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/settings/screen/options/OptionsScreenUiState.kt
@@ -1,7 +1,6 @@
package eu.m724.pojavbackup.settings.screen.options
data class OptionsScreenUiState(
- val improvement: Boolean = false,
val backupStatusHeader: String = "",
val backupStatusDetails: String = ""
)
diff --git a/app/src/main/java/eu/m724/pojavbackup/settings/screen/options/OptionsScreenViewModel.kt b/app/src/main/java/eu/m724/pojavbackup/settings/screen/options/OptionsScreenViewModel.kt
index 7a7ce21..825f3c8 100644
--- a/app/src/main/java/eu/m724/pojavbackup/settings/screen/options/OptionsScreenViewModel.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/settings/screen/options/OptionsScreenViewModel.kt
@@ -29,18 +29,6 @@ class OptionsScreenViewModel @Inject constructor(
private val workManager = WorkManager
.getInstance(appContext)
- init {
- viewModelScope.launch {
- settingsRepository.getSettingsFlow().collect { settings ->
- _uiState.update {
- it.copy(
- improvement = settings.improvement
- )
- }
- }
- }
- }
-
fun backupNow() {
_uiState.update {
it.copy(
@@ -77,12 +65,4 @@ class OptionsScreenViewModel @Inject constructor(
}
}
}
-
- fun setOptions(
- improvement: Boolean? = true
- ) {
- viewModelScope.launch {
- settingsRepository.updateSettings(improvement = improvement)
- }
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/setup/SetupActivity.kt b/app/src/main/java/eu/m724/pojavbackup/setup/SetupActivity.kt
index aee669c..7e03dda 100644
--- a/app/src/main/java/eu/m724/pojavbackup/setup/SetupActivity.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/setup/SetupActivity.kt
@@ -23,6 +23,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -30,6 +31,7 @@ import androidx.compose.ui.unit.sp
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dagger.hilt.android.AndroidEntryPoint
+import eu.m724.pojavbackup.R
import eu.m724.pojavbackup.home.HomeActivity
import eu.m724.pojavbackup.ui.theme.PojavBackupTheme
@@ -113,25 +115,25 @@ fun SetupScreen(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
- text = "Setup",
+ text = stringResource(R.string.header_setup),
modifier = Modifier.padding(vertical = 30.dp),
fontSize = 24.sp
)
GrantCard(
- title = "Storage permission",
- description = "It's needed to access your saves etc.",
+ title = stringResource(R.string.permission_storage),
+ description = stringResource(R.string.permission_storage_description),
granted = uiState.storagePermissionGranted,
onClick = onStoragePermissionGrantClick
)
GrantCard(
- title = "Notification permission",
- description = "It's needed to notify you about backup status.",
+ title = stringResource(R.string.permission_notification),
+ description = stringResource(R.string.permission_notification_description),
granted = uiState.notificationPermissionGranted || uiState.notificationPermissionRejected,
onClick = onNotificationPermissionGrantClick,
- customButtonText = if (uiState.notificationPermissionRejected) "Rejected" else null
+ customButtonText = if (uiState.notificationPermissionRejected) stringResource(R.string.permission_rejected) else null
)
}
}
@@ -178,9 +180,9 @@ fun GrantCard(
) {
if (customButtonText == null) {
if (granted) {
- Text("Already granted")
+ Text(stringResource(R.string.permission_granted))
} else {
- Text("Grant")
+ Text(stringResource(R.string.permission_grant))
}
} else {
Text(customButtonText)
diff --git a/app/src/main/proto/settings.proto b/app/src/main/proto/settings.proto
index 3b91cf4..b1f5089 100644
--- a/app/src/main/proto/settings.proto
+++ b/app/src/main/proto/settings.proto
@@ -20,7 +20,6 @@ message Settings {
repeated BackupDestination destinations = 3;
string sourceUri = 4;
- bool improvement = 5;
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 478a0ae..c642710 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,6 +1,28 @@
PojavBackup
- SetupActivity
- HomeActivity
- SettingsActivity
+ Notifications permission
+ It\'s needed to notify you about backup status.
+ Rejected
+ It\'s needed to access your saves etc.
+ Storage permission
+ Setup
+ Granted
+ Grant
+ Exit settings
+ Settings
+ Options
+ Content
+ Destination
+ Backup now
+ No destinations
+ Add destination
+ No worlds available
+ Worlds above this line will be backed up
+ Last played: %1$s
+ Dashboard
+ History
+ Last backup
+ Health
+ Worlds
+ Go to %1$s
\ No newline at end of file