diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eb198b8ea..837ee29cb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,23 +8,23 @@ - + @@ -104,6 +104,7 @@ android:windowSoftInputMode="adjustResize" /> diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt index dc390f0f3..dadbb05eb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt @@ -1,28 +1,9 @@ package org.koitharu.kotatsu.core.db -import androidx.room.Room import org.koin.android.ext.koin.androidContext import org.koin.dsl.module -import org.koitharu.kotatsu.core.db.migrations.* val databaseModule get() = module { - single { - Room.databaseBuilder( - androidContext(), - MangaDatabase::class.java, - "kotatsu-db" - ).addMigrations( - Migration1To2(), - Migration2To3(), - Migration3To4(), - Migration4To5(), - Migration5To6(), - Migration6To7(), - Migration7To8(), - Migration8To9(), - ).addCallback( - DatabasePrePopulateCallback(androidContext().resources) - ).build() - } + single { MangaDatabase.create(androidContext()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt index 075e6d02a..010a2653f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt @@ -1,9 +1,12 @@ package org.koitharu.kotatsu.core.db +import android.content.Context import androidx.room.Database +import androidx.room.Room import androidx.room.RoomDatabase import org.koitharu.kotatsu.core.db.dao.* import org.koitharu.kotatsu.core.db.entity.* +import org.koitharu.kotatsu.core.db.migrations.* import org.koitharu.kotatsu.favourites.data.FavouriteCategoriesDao import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity import org.koitharu.kotatsu.favourites.data.FavouriteEntity @@ -40,4 +43,24 @@ abstract class MangaDatabase : RoomDatabase() { abstract val trackLogsDao: TrackLogsDao abstract val suggestionDao: SuggestionDao + + companion object { + + fun create(context: Context): MangaDatabase = Room.databaseBuilder( + context, + MangaDatabase::class.java, + "kotatsu-db" + ).addMigrations( + Migration1To2(), + Migration2To3(), + Migration3To4(), + Migration4To5(), + Migration5To6(), + Migration6To7(), + Migration7To8(), + Migration8To9(), + ).addCallback( + DatabasePrePopulateCallback(context.resources) + ).build() + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt new file mode 100644 index 000000000..9b59a983c --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt @@ -0,0 +1,107 @@ +package org.koitharu.kotatsu.settings.backup + +import android.app.backup.BackupAgent +import android.app.backup.BackupDataInput +import android.app.backup.BackupDataOutput +import android.app.backup.FullBackupDataOutput +import android.os.ParcelFileDescriptor +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.runBlocking +import org.koitharu.kotatsu.core.backup.BackupArchive +import org.koitharu.kotatsu.core.backup.BackupEntry +import org.koitharu.kotatsu.core.backup.BackupRepository +import org.koitharu.kotatsu.core.backup.RestoreRepository +import org.koitharu.kotatsu.core.db.MangaDatabase +import java.io.* + +class AppBackupAgent : BackupAgent() { + + override fun onBackup( + oldState: ParcelFileDescriptor?, + data: BackupDataOutput?, + newState: ParcelFileDescriptor? + ) = Unit + + override fun onRestore( + data: BackupDataInput?, + appVersionCode: Int, + newState: ParcelFileDescriptor? + ) = Unit + + override fun onFullBackup(data: FullBackupDataOutput) { + super.onFullBackup(data) + val file = createBackupFile() + try { + fullBackupFile(file, data) + } finally { + file.delete() + } + } + + override fun onRestoreFile( + data: ParcelFileDescriptor, + size: Long, + destination: File?, + type: Int, + mode: Long, + mtime: Long + ) { + if (destination?.name?.endsWith(".bak") == true) { + restoreBackupFile(data.fileDescriptor, size) + destination.delete() + } else { + super.onRestoreFile(data, size, destination, type, mode, mtime) + } + } + + private fun createBackupFile() = runBlocking { + val repository = BackupRepository(MangaDatabase.create(applicationContext)) + val backup = BackupArchive.createNew(this@AppBackupAgent) + backup.put(repository.createIndex()) + backup.put(repository.dumpHistory()) + backup.put(repository.dumpCategories()) + backup.put(repository.dumpFavourites()) + backup.flush() + backup.cleanup() + backup.file + } + + private fun restoreBackupFile(fd: FileDescriptor, size: Long) { + val repository = RestoreRepository(MangaDatabase.create(applicationContext)) + val tempFile = File.createTempFile("backup_", ".tmp") + FileInputStream(fd).use { input -> + tempFile.outputStream().use { output -> + input.copyLimitedTo(output, size) + } + } + val backup = BackupArchive(tempFile) + try { + runBlocking { + backup.unpack() + repository.upsertHistory(backup.getEntry(BackupEntry.HISTORY)) + repository.upsertCategories(backup.getEntry(BackupEntry.CATEGORIES)) + repository.upsertFavourites(backup.getEntry(BackupEntry.FAVOURITES)) + } + } finally { + runBlocking(NonCancellable) { + backup.cleanup() + } + tempFile.delete() + } + } + + private fun InputStream.copyLimitedTo(out: OutputStream, limit: Long) { + var bytesCopied: Long = 0 + val buffer = ByteArray(DEFAULT_BUFFER_SIZE.coerceAtMost(limit.toInt())) + var bytes = read(buffer) + while (bytes >= 0) { + out.write(buffer, 0, bytes) + bytesCopied += bytes + val bytesLeft = (limit - bytesCopied).toInt() + if (bytesLeft <= 0) { + break + } + bytes = read(buffer, 0, buffer.size.coerceAtMost(bytesLeft)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/MutableZipFile.kt b/app/src/main/java/org/koitharu/kotatsu/utils/MutableZipFile.kt index b785a62d2..01eaf7118 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/MutableZipFile.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/MutableZipFile.kt @@ -1,6 +1,5 @@ package org.koitharu.kotatsu.utils -import androidx.annotation.CheckResult import androidx.annotation.WorkerThread import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible @@ -44,7 +43,6 @@ open class MutableZipFile(val file: File) { dir.deleteRecursively() } - @CheckResult suspend fun flush(): Boolean = runInterruptible(Dispatchers.IO) { val tempFile = File(file.path + ".tmp") if (tempFile.exists()) { diff --git a/app/src/main/res/xml/backup_content.xml b/app/src/main/res/xml/backup_content.xml new file mode 100644 index 000000000..f2c5989ed --- /dev/null +++ b/app/src/main/res/xml/backup_content.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/xml/backup_descriptor.xml b/app/src/main/res/xml/backup_descriptor.xml deleted file mode 100644 index 1b0854f7d..000000000 --- a/app/src/main/res/xml/backup_descriptor.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 000000000..5c0b063ba --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file