Improve android AutoBackup support
This commit is contained in:
@@ -8,23 +8,23 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
|
||||||
android:maxSdkVersion="28"
|
|
||||||
tools:ignore="ScopedStorage" />
|
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="org.koitharu.kotatsu.KotatsuApp"
|
android:name="org.koitharu.kotatsu.KotatsuApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:fullBackupContent="@xml/backup_descriptor"
|
android:backupAgent="org.koitharu.kotatsu.settings.backup.AppBackupAgent"
|
||||||
|
android:dataExtractionRules="@xml/backup_rules"
|
||||||
|
android:fullBackupContent="@xml/backup_content"
|
||||||
|
android:fullBackupOnly="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Kotatsu"
|
android:theme="@style/Theme.Kotatsu"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
|
||||||
tools:ignore="UnusedAttribute">
|
tools:ignore="UnusedAttribute">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.main.ui.MainActivity"
|
android:name="org.koitharu.kotatsu.main.ui.MainActivity"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
@@ -104,6 +104,7 @@
|
|||||||
android:windowSoftInputMode="adjustResize" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.download.ui.DownloadsActivity"
|
android:name="org.koitharu.kotatsu.download.ui.DownloadsActivity"
|
||||||
|
android:launchMode="singleTop"
|
||||||
android:label="@string/downloads" />
|
android:label="@string/downloads" />
|
||||||
<activity android:name="org.koitharu.kotatsu.image.ui.ImageActivity"/>
|
<activity android:name="org.koitharu.kotatsu.image.ui.ImageActivity"/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,9 @@
|
|||||||
package org.koitharu.kotatsu.core.db
|
package org.koitharu.kotatsu.core.db
|
||||||
|
|
||||||
import androidx.room.Room
|
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import org.koitharu.kotatsu.core.db.migrations.*
|
|
||||||
|
|
||||||
val databaseModule
|
val databaseModule
|
||||||
get() = module {
|
get() = module {
|
||||||
single {
|
single { MangaDatabase.create(androidContext()) }
|
||||||
Room.databaseBuilder(
|
|
||||||
androidContext(),
|
|
||||||
MangaDatabase::class.java,
|
|
||||||
"kotatsu-db"
|
|
||||||
).addMigrations(
|
|
||||||
Migration1To2(),
|
|
||||||
Migration2To3(),
|
|
||||||
Migration3To4(),
|
|
||||||
Migration4To5(),
|
|
||||||
Migration5To6(),
|
|
||||||
Migration6To7(),
|
|
||||||
Migration7To8(),
|
|
||||||
Migration8To9(),
|
|
||||||
).addCallback(
|
|
||||||
DatabasePrePopulateCallback(androidContext().resources)
|
|
||||||
).build()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
package org.koitharu.kotatsu.core.db
|
package org.koitharu.kotatsu.core.db
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import org.koitharu.kotatsu.core.db.dao.*
|
import org.koitharu.kotatsu.core.db.dao.*
|
||||||
import org.koitharu.kotatsu.core.db.entity.*
|
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.FavouriteCategoriesDao
|
||||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||||
@@ -40,4 +43,24 @@ abstract class MangaDatabase : RoomDatabase() {
|
|||||||
abstract val trackLogsDao: TrackLogsDao
|
abstract val trackLogsDao: TrackLogsDao
|
||||||
|
|
||||||
abstract val suggestionDao: SuggestionDao
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package org.koitharu.kotatsu.utils
|
package org.koitharu.kotatsu.utils
|
||||||
|
|
||||||
import androidx.annotation.CheckResult
|
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
@@ -44,7 +43,6 @@ open class MutableZipFile(val file: File) {
|
|||||||
dir.deleteRecursively()
|
dir.deleteRecursively()
|
||||||
}
|
}
|
||||||
|
|
||||||
@CheckResult
|
|
||||||
suspend fun flush(): Boolean = runInterruptible(Dispatchers.IO) {
|
suspend fun flush(): Boolean = runInterruptible(Dispatchers.IO) {
|
||||||
val tempFile = File(file.path + ".tmp")
|
val tempFile = File(file.path + ".tmp")
|
||||||
if (tempFile.exists()) {
|
if (tempFile.exists()) {
|
||||||
|
|||||||
6
app/src/main/res/xml/backup_content.xml
Normal file
6
app/src/main/res/xml/backup_content.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<full-backup-content>
|
||||||
|
<include
|
||||||
|
domain="sharedpref"
|
||||||
|
path="." />
|
||||||
|
</full-backup-content>
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<full-backup-content />
|
|
||||||
6
app/src/main/res/xml/backup_rules.xml
Normal file
6
app/src/main/res/xml/backup_rules.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<data-extraction-rules>
|
||||||
|
<cloud-backup disableIfNoEncryptionCapabilities="false">
|
||||||
|
<include domain="sharedpref" path="."/>
|
||||||
|
</cloud-backup>
|
||||||
|
</data-extraction-rules>
|
||||||
Reference in New Issue
Block a user