Update
This commit is contained in:
parent
34af1f025f
commit
462c49765b
17 changed files with 263 additions and 41 deletions
|
|
@ -62,6 +62,9 @@ dependencies {
|
||||||
implementation(libs.androidx.hilt.navigation.compose)
|
implementation(libs.androidx.hilt.navigation.compose)
|
||||||
implementation(libs.androidx.datastore)
|
implementation(libs.androidx.datastore)
|
||||||
implementation(libs.protobuf.javalite)
|
implementation(libs.protobuf.javalite)
|
||||||
|
implementation(libs.commons.compress)
|
||||||
|
implementation(libs.androidx.work.runtime.ktx)
|
||||||
|
implementation(libs.hilt.work)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,17 @@
|
||||||
package eu.m724.pojavbackup
|
package eu.m724.pojavbackup
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import androidx.hilt.work.HiltWorkerFactory
|
||||||
|
import androidx.work.Configuration
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class PojavBackupApplication : Application() {
|
class PojavBackupApplication : Application(), Configuration.Provider {
|
||||||
|
@Inject lateinit var workerFactory: HiltWorkerFactory
|
||||||
|
|
||||||
|
override val workManagerConfiguration: Configuration
|
||||||
|
get() = Configuration.Builder()
|
||||||
|
.setWorkerFactory(workerFactory)
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
package eu.m724.pojavbackup.core
|
|
||||||
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
data class Backup(
|
|
||||||
val timestamp: Instant,
|
|
||||||
val status: BackupStatus
|
|
||||||
) {
|
|
||||||
|
|
||||||
}
|
|
||||||
13
app/src/main/java/eu/m724/pojavbackup/core/backup/Backup.kt
Normal file
13
app/src/main/java/eu/m724/pojavbackup/core/backup/Backup.kt
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
package eu.m724.pojavbackup.core.backup
|
||||||
|
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
data class Backup(
|
||||||
|
val id: String,
|
||||||
|
val timestamp: Instant,
|
||||||
|
val status: BackupStatus,
|
||||||
|
val tempDirectory: Path?
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
package eu.m724.pojavbackup.core.backup
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
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.datastore.SettingsRepository
|
||||||
|
import java.io.File
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.HexFormat
|
||||||
|
import java.util.concurrent.ThreadLocalRandom
|
||||||
|
import javax.inject.Singleton
|
||||||
|
import kotlin.io.path.ExperimentalPathApi
|
||||||
|
import kotlin.io.path.deleteRecursively
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object BackupModule {
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideBackupRepository(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
settingsRepository: SettingsRepository
|
||||||
|
): BackupRepository {
|
||||||
|
return object : BackupRepository {
|
||||||
|
override suspend fun createBackup(): Backup {
|
||||||
|
val bytes = ByteArray(16)
|
||||||
|
ThreadLocalRandom.current().nextBytes(bytes)
|
||||||
|
|
||||||
|
val id = HexFormat.of().formatHex(bytes)
|
||||||
|
|
||||||
|
val path = File.createTempFile("bp-$id", null, context.cacheDir).toPath()
|
||||||
|
|
||||||
|
return Backup(
|
||||||
|
id,
|
||||||
|
Instant.now(),
|
||||||
|
BackupStatus.ONGOING,
|
||||||
|
path
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPathApi::class)
|
||||||
|
override suspend fun completeBackup(backup: Backup, status: BackupStatus): Backup {
|
||||||
|
backup.tempDirectory!!.deleteRecursively()
|
||||||
|
|
||||||
|
return backup.copy(
|
||||||
|
status = status,
|
||||||
|
tempDirectory = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun backupDirectory(backup: Backup, directory: String) {
|
||||||
|
val documentFile = DocumentFile.fromTreeUri(context, settingsRepository.getSettings().sourceUri.toUri())
|
||||||
|
|
||||||
|
PathTools.copyFromDocumentFileToPath(
|
||||||
|
context,
|
||||||
|
documentFile!!.findFile(directory)!!,
|
||||||
|
backup.tempDirectory!!.resolve(directory)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package eu.m724.pojavbackup.core.backup
|
||||||
|
|
||||||
|
interface BackupRepository {
|
||||||
|
suspend fun createBackup(): Backup
|
||||||
|
suspend fun completeBackup(backup: Backup, status: BackupStatus = BackupStatus.SUCCESS): Backup
|
||||||
|
|
||||||
|
suspend fun backupDirectory(backup: Backup, directory: String)
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.m724.pojavbackup.core
|
package eu.m724.pojavbackup.core.backup
|
||||||
|
|
||||||
enum class BackupStatus {
|
enum class BackupStatus {
|
||||||
SUCCESS,
|
SUCCESS,
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package eu.m724.pojavbackup.core.backup
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.hilt.work.HiltWorker
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
|
||||||
|
@HiltWorker
|
||||||
|
class BackupWorker @AssistedInject constructor(
|
||||||
|
@Assisted appContext: Context,
|
||||||
|
@Assisted workerParams: WorkerParameters,
|
||||||
|
private val backupRepository: BackupRepository
|
||||||
|
|
||||||
|
) : CoroutineWorker(appContext, workerParams) {
|
||||||
|
companion object {
|
||||||
|
const val TAG = "BackupWorker"
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
val backup = backupRepository.createBackup()
|
||||||
|
|
||||||
|
return try {
|
||||||
|
Log.d(TAG, "Created backup: ${backup.id}")
|
||||||
|
|
||||||
|
backupRepository.completeBackup(backup, BackupStatus.SUCCESS)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Result.success()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
backupRepository.completeBackup(backup, BackupStatus.FAILURE)
|
||||||
|
|
||||||
|
Result.failure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
package eu.m724.pojavbackup.core.backup
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import org.apache.commons.compress.archivers.ArchiveEntry
|
||||||
|
import org.apache.commons.compress.archivers.ArchiveOutputStream
|
||||||
|
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream
|
||||||
|
import org.apache.commons.compress.compressors.zstandard.ZstdCompressorOutputStream
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.attribute.FileTime
|
||||||
|
import kotlin.io.path.createDirectory
|
||||||
|
import kotlin.io.path.name
|
||||||
|
import kotlin.io.path.outputStream
|
||||||
|
import kotlin.io.path.walk
|
||||||
|
|
||||||
|
class PathTools {
|
||||||
|
companion object {
|
||||||
|
fun compressDocumentFileDirectory(source: Path, target: Path) {
|
||||||
|
target.outputStream().use {
|
||||||
|
ZstdCompressorOutputStream(it).use {
|
||||||
|
TarArchiveOutputStream(it).use { outputStream ->
|
||||||
|
compressInner(source, outputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T : ArchiveEntry> compressInner(source: Path, outputStream: ArchiveOutputStream<T>) {
|
||||||
|
// TODO we could compress DirectoryFile I think https://commons.apache.org/proper/commons-compress/examples.html https://aistudio.google.com/prompts/1AFCTIE9FdxT3AvDOQ0puTxoSv6xMYYH1
|
||||||
|
source.walk().forEach {
|
||||||
|
val entry = outputStream.createArchiveEntry(it, it.name)
|
||||||
|
outputStream.putArchiveEntry(entry)
|
||||||
|
Files.copy(it, outputStream)
|
||||||
|
outputStream.closeArchiveEntry()
|
||||||
|
}
|
||||||
|
|
||||||
|
outputStream.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively copies the contents of a DocumentFile directory to a local Path.
|
||||||
|
*
|
||||||
|
* @param context Context needed for ContentResolver.
|
||||||
|
* @param sourceDir The source DocumentFile directory. Must be a directory.
|
||||||
|
* @param targetDir The destination Path directory. Will be created if it doesn't exist.
|
||||||
|
*/
|
||||||
|
fun copyFromDocumentFileToPath(
|
||||||
|
context: Context,
|
||||||
|
sourceDir: DocumentFile,
|
||||||
|
targetDir: Path
|
||||||
|
) {
|
||||||
|
if (!sourceDir.isDirectory) {
|
||||||
|
// TODO copy file
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceDir.listFiles().forEach { child ->
|
||||||
|
val targetPath = targetDir.resolve(child.name)
|
||||||
|
|
||||||
|
if (child.isDirectory) {
|
||||||
|
targetPath.createDirectory()
|
||||||
|
copyFromDocumentFileToPath(context, child, targetPath)
|
||||||
|
} else {
|
||||||
|
context.contentResolver.openInputStream(child.uri)!!.use { inputStream ->
|
||||||
|
Files.copy(inputStream, targetPath)
|
||||||
|
Files.setLastModifiedTime(targetPath, FileTime.fromMillis(child.lastModified()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.m724.pojavbackup.core
|
package eu.m724.pojavbackup.core.data
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
|
@ -12,35 +12,37 @@ import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class WorldRepository @Inject constructor(
|
class LauncherDataRepository @Inject constructor(
|
||||||
@ApplicationContext private val appContext: Context
|
@ApplicationContext private val appContext: Context
|
||||||
) {
|
) {
|
||||||
private lateinit var savesDirectory: DocumentFile
|
private lateinit var dataDirectory: DocumentFile
|
||||||
private lateinit var worldDetector: WorldDetector
|
private lateinit var worldScanner: WorldScanner
|
||||||
|
|
||||||
private var worldsCache: List<World>? = null
|
private var worldCache: List<World>? = null
|
||||||
private val cacheMutex = Mutex() // To ensure thread-safe access to cache
|
private val worldCacheMutex = Mutex() // To ensure thread-safe access to cache
|
||||||
|
|
||||||
fun setSavesDirectory(documentFile: DocumentFile) {
|
fun setSavesDirectory(documentFile: DocumentFile) {
|
||||||
this.savesDirectory = documentFile
|
this.dataDirectory = documentFile
|
||||||
this.worldDetector = WorldDetector(appContext.contentResolver, savesDirectory)
|
|
||||||
|
val savesDirectory = dataDirectory.findFile(".minecraft")!!.findFile("saves")!!
|
||||||
|
this.worldScanner = WorldScanner(appContext.contentResolver, savesDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun listWorlds(): List<World> {
|
suspend fun listWorlds(): List<World> {
|
||||||
cacheMutex.withLock {
|
worldCacheMutex.withLock {
|
||||||
if (worldsCache != null) {
|
if (worldCache != null) {
|
||||||
return worldsCache!! // Return copy or immutable list if needed
|
return worldCache!! // Return copy or immutable list if needed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If cache is empty, fetch data on IO dispatcher
|
// If cache is empty, fetch data on IO dispatcher
|
||||||
val freshData = withContext(Dispatchers.IO) {
|
val freshData = withContext(Dispatchers.IO) {
|
||||||
worldDetector.listWorlds().toList() // TODO
|
worldScanner.listWorlds().toList() // TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store in cache (thread-safe)
|
// Store in cache (thread-safe)
|
||||||
cacheMutex.withLock {
|
worldCacheMutex.withLock {
|
||||||
worldsCache = freshData
|
worldCache = freshData
|
||||||
}
|
}
|
||||||
|
|
||||||
return freshData
|
return freshData
|
||||||
|
|
@ -51,8 +53,8 @@ class WorldRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun clearCache() {
|
suspend fun clearCache() {
|
||||||
cacheMutex.withLock {
|
worldCacheMutex.withLock {
|
||||||
worldsCache = null
|
worldCache = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.m724.pojavbackup.core
|
package eu.m724.pojavbackup.core.data
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
@ -8,7 +8,7 @@ import java.time.ZonedDateTime
|
||||||
data class World(
|
data class World(
|
||||||
val id: String,
|
val id: String,
|
||||||
val displayName: String,
|
val displayName: String,
|
||||||
val lastPlayed: ZonedDateTime, // TODO or Instant?
|
val lastPlayed: ZonedDateTime,
|
||||||
val icon: Bitmap?
|
val icon: Bitmap?
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.m724.pojavbackup.core
|
package eu.m724.pojavbackup.core.data
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
|
@ -17,7 +17,7 @@ import net.benwoodworth.knbt.nbtString
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
class WorldDetector(
|
class WorldScanner(
|
||||||
private val contentResolver: ContentResolver,
|
private val contentResolver: ContentResolver,
|
||||||
private val savesDirectory: DocumentFile
|
private val savesDirectory: DocumentFile
|
||||||
) {
|
) {
|
||||||
|
|
@ -9,7 +9,7 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import eu.m724.pojavbackup.core.WorldRepository
|
import eu.m724.pojavbackup.core.data.LauncherDataRepository
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
@ -22,7 +22,7 @@ import javax.inject.Inject
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class HomeViewModel @Inject constructor(
|
class HomeViewModel @Inject constructor(
|
||||||
@ApplicationContext private val appContext: Context,
|
@ApplicationContext private val appContext: Context,
|
||||||
private val worldRepository: WorldRepository
|
private val launcherDataRepository: LauncherDataRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val TAG = javaClass.name
|
private val TAG = javaClass.name
|
||||||
|
|
||||||
|
|
@ -46,8 +46,8 @@ class HomeViewModel @Inject constructor(
|
||||||
.findFile("saves")
|
.findFile("saves")
|
||||||
|
|
||||||
if (documentFile != null) {
|
if (documentFile != null) {
|
||||||
worldRepository.setSavesDirectory(documentFile)
|
launcherDataRepository.setSavesDirectory(documentFile)
|
||||||
worldRepository.listWorlds()
|
launcherDataRepository.listWorlds()
|
||||||
} else {
|
} else {
|
||||||
// TODO handle if "saves" doesn't exist
|
// TODO handle if "saves" doesn't exist
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.m724.pojavbackup.R
|
import eu.m724.pojavbackup.R
|
||||||
import eu.m724.pojavbackup.core.BackupStatus
|
import eu.m724.pojavbackup.core.backup.BackupStatus
|
||||||
import eu.m724.pojavbackup.home.screen.ScreenColumn
|
import eu.m724.pojavbackup.home.screen.ScreenColumn
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import eu.m724.pojavbackup.core.World
|
import eu.m724.pojavbackup.core.data.World
|
||||||
import eu.m724.pojavbackup.core.WorldRepository
|
import eu.m724.pojavbackup.core.data.LauncherDataRepository
|
||||||
import eu.m724.pojavbackup.core.datastore.SettingsRepository
|
import eu.m724.pojavbackup.core.datastore.SettingsRepository
|
||||||
import eu.m724.pojavbackup.proto.WorldOrder
|
import eu.m724.pojavbackup.proto.WorldOrder
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
@ -22,7 +22,7 @@ import javax.inject.Inject
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ContentScreenViewModel @Inject constructor(
|
class ContentScreenViewModel @Inject constructor(
|
||||||
@ApplicationContext private val appContext: Context,
|
@ApplicationContext private val appContext: Context,
|
||||||
private val worldRepository: WorldRepository,
|
private val launcherDataRepository: LauncherDataRepository,
|
||||||
private val settingsRepository: SettingsRepository
|
private val settingsRepository: SettingsRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _worlds = MutableStateFlow<List<World>>(emptyList())
|
private val _worlds = MutableStateFlow<List<World>>(emptyList())
|
||||||
|
|
@ -33,7 +33,7 @@ class ContentScreenViewModel @Inject constructor(
|
||||||
settingsRepository.getSettingsFlow().collect { settings ->
|
settingsRepository.getSettingsFlow().collect { settings ->
|
||||||
val worlds = settings.worldOrder.worldIdsList.map {
|
val worlds = settings.worldOrder.worldIdsList.map {
|
||||||
// TODO mark deleted worlds better
|
// TODO mark deleted worlds better
|
||||||
worldRepository.getWorld(it) ?: World(it, "Deleted world", Instant.EPOCH.atZone(ZoneOffset.UTC), null)
|
launcherDataRepository.getWorld(it) ?: World(it, "Deleted world", Instant.EPOCH.atZone(ZoneOffset.UTC), null)
|
||||||
}.toMutableList()
|
}.toMutableList()
|
||||||
worlds.add(settings.worldOrder.separatorIndex, World.SEPARATOR)
|
worlds.add(settings.worldOrder.separatorIndex, World.SEPARATOR)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,17 @@ message Settings {
|
||||||
repeated string extraPaths = 2;
|
repeated string extraPaths = 2;
|
||||||
|
|
||||||
repeated BackupDestination destinations = 3;
|
repeated BackupDestination destinations = 3;
|
||||||
|
|
||||||
|
string sourceUri = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
message BackupMeta {
|
||||||
|
string id = 1;
|
||||||
|
int64 timestamp = 2;
|
||||||
|
int32 status = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message BackupsMeta {
|
||||||
|
repeated BackupMeta backups = 1;
|
||||||
}
|
}
|
||||||
|
|
@ -20,6 +20,9 @@ hiltNavigationCompose = "1.2.0"
|
||||||
datastore = "1.1.4"
|
datastore = "1.1.4"
|
||||||
protobufJavalite = "4.30.2"
|
protobufJavalite = "4.30.2"
|
||||||
protobuf = "0.9.5"
|
protobuf = "0.9.5"
|
||||||
|
commonsCompress = "1.27.1"
|
||||||
|
work = "2.10.0"
|
||||||
|
hiltWork = "1.2.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
|
|
@ -47,6 +50,9 @@ hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.r
|
||||||
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose"}
|
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose"}
|
||||||
androidx-datastore = { group = "androidx.datastore", name = "datastore", version.ref = "datastore" }
|
androidx-datastore = { group = "androidx.datastore", name = "datastore", version.ref = "datastore" }
|
||||||
protobuf-javalite = { group = "com.google.protobuf", name = "protobuf-javalite", version.ref = "protobufJavalite"}
|
protobuf-javalite = { group = "com.google.protobuf", name = "protobuf-javalite", version.ref = "protobufJavalite"}
|
||||||
|
commons-compress = { group = "org.apache.commons", name = "commons-compress", version.ref = "commonsCompress"}
|
||||||
|
androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" }
|
||||||
|
hilt-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "hiltWork"}
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue