From 392c0efc83640cfe5fbd5d8ed1c62d598575c259 Mon Sep 17 00:00:00 2001 From: Minecon724 Date: Mon, 28 Apr 2025 20:13:34 +0200 Subject: [PATCH] Refactor --- app/src/main/AndroidManifest.xml | 3 - .../pojavbackup/core/backup/BackupModule.kt | 12 +- .../core/backup/BackupRepository.kt | 4 +- .../pojavbackup/core/backup/BackupWorker.kt | 167 +++++---- .../core/backup/DocumentFileCompressor.kt | 54 +++ .../m724/pojavbackup/core/backup/McaUtils.kt | 349 ------------------ .../m724/pojavbackup/core/backup/PathTools.kt | 77 ---- .../core/backup/exception/BackupException.kt | 8 + .../backup/exception/BackupFailedException.kt | 9 + .../exception/BackupOfWorldException.kt | 8 + .../core/datastore/SettingsRepository.kt | 16 - .../eu/m724/pojavbackup/home/HomeActivity.kt | 7 +- .../home/screen/dashboard/DashboardScreen.kt | 13 +- .../home/screen/history/HistoryScreen.kt | 2 +- .../pojavbackup/settings/SettingsActivity.kt | 11 +- .../settings/screen/content/ContentScreen.kt | 31 +- .../screen/destination/DestinationScreen.kt | 7 +- .../settings/screen/options/OptionsScreen.kt | 39 +- .../screen/options/OptionsScreenUiState.kt | 1 - .../screen/options/OptionsScreenViewModel.kt | 20 - .../m724/pojavbackup/setup/SetupActivity.kt | 18 +- app/src/main/proto/settings.proto | 1 - app/src/main/res/values/strings.xml | 28 +- 23 files changed, 240 insertions(+), 645 deletions(-) create mode 100644 app/src/main/java/eu/m724/pojavbackup/core/backup/DocumentFileCompressor.kt delete mode 100644 app/src/main/java/eu/m724/pojavbackup/core/backup/McaUtils.kt delete mode 100644 app/src/main/java/eu/m724/pojavbackup/core/backup/PathTools.kt create mode 100644 app/src/main/java/eu/m724/pojavbackup/core/backup/exception/BackupException.kt create mode 100644 app/src/main/java/eu/m724/pojavbackup/core/backup/exception/BackupFailedException.kt create mode 100644 app/src/main/java/eu/m724/pojavbackup/core/backup/exception/BackupOfWorldException.kt 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