diff --git a/app/src/main/java/foundation/e/apps/data/di/bindings/PreferenceBindingModule.kt b/app/src/main/java/foundation/e/apps/data/di/bindings/PreferenceBindingModule.kt index 9029aa2dcfe29028fd3ff7a39b6be2a99e5279ba..5e0a42168d833fc62d3862526fcc2167d65ee9bc 100644 --- a/app/src/main/java/foundation/e/apps/data/di/bindings/PreferenceBindingModule.kt +++ b/app/src/main/java/foundation/e/apps/data/di/bindings/PreferenceBindingModule.kt @@ -22,10 +22,12 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import foundation.e.apps.data.debug.DatabaseDumpRepositoryImpl import foundation.e.apps.data.preference.AppLoungePreference import foundation.e.apps.data.preference.PlayStoreAuthStore import foundation.e.apps.data.preference.SessionDataStore import foundation.e.apps.data.preference.updateinterval.UpdatePreferencesRepositoryImpl +import foundation.e.apps.domain.debug.DatabaseDumpRepository import foundation.e.apps.domain.login.PlayStoreCredentialsRepository import foundation.e.apps.domain.preferences.AppPreferencesRepository import foundation.e.apps.domain.preferences.SessionRepository @@ -54,6 +56,12 @@ interface PreferenceBindingModule { updatePreferencesRepositoryImpl: UpdatePreferencesRepositoryImpl ): UpdatePreferencesRepository + @Binds + @Singleton + fun bindDatabaseDumpRepository( + databaseDumpRepositoryImpl: DatabaseDumpRepositoryImpl + ): DatabaseDumpRepository + @Binds @Singleton fun bindPlayStoreAuthStore( diff --git a/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt b/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt index f2ca68226c998ac93cc958e65cedc8aa92a2e71b..8cf81c06f3a3eec7138a462d8d4a862a43b3ac8f 100644 --- a/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt @@ -26,8 +26,11 @@ import android.view.View import android.widget.Toast import androidx.core.net.toUri import androidx.core.view.isVisible +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.findNavController import androidx.preference.CheckBoxPreference import androidx.preference.ListPreference @@ -52,6 +55,7 @@ import foundation.e.apps.databinding.CustomPreferenceBinding import foundation.e.apps.domain.model.User import foundation.e.apps.domain.preferences.SessionRepository import foundation.e.apps.ui.LoginViewModel +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import timber.log.Timber import java.util.Locale @@ -66,6 +70,8 @@ class SettingsFragment : PreferenceFragmentCompat() { private var showPWAApplications: CheckBoxPreference? = null private var troubleShootPreference: Preference? = null + private val settingsViewModel: SettingsViewModel by viewModels() + private val loginViewModel: LoginViewModel by lazy { ViewModelProvider(requireActivity())[LoginViewModel::class.java] } @@ -122,6 +128,10 @@ class SettingsFragment : PreferenceFragmentCompat() { val versionInfo = findPreference("versionInfo") versionInfo?.apply { summary = BuildConfig.VERSION_NAME + setOnPreferenceClickListener { + settingsViewModel.onVersionInfoTapped() + true + } setOnLongClickListener { val osVersion = SystemInfoProvider.getSystemProperty(SystemInfoProvider.KEY_LINEAGE_VERSION) val appVersionLabel = getString(R.string.app_version_label) @@ -150,6 +160,49 @@ class SettingsFragment : PreferenceFragmentCompat() { troubleShootPreference?.intent = Intent(Intent.ACTION_VIEW, troubleshootUrl.toUri()) } + private fun observeSettingsEffects() { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + settingsViewModel.effect.collectLatest(::handleSettingsEffect) + } + } + } + + private fun handleSettingsEffect(effect: SettingsUiEffect) { + when (effect) { + SettingsUiEffect.DatabaseDumpStarted -> { + Toast.makeText(requireContext(), R.string.database_dump_started, Toast.LENGTH_SHORT) + .show() + } + + SettingsUiEffect.DatabaseDumpInProgress -> { + Toast.makeText( + requireContext(), + R.string.database_dump_in_progress, + Toast.LENGTH_SHORT + ).show() + } + + is SettingsUiEffect.DatabaseDumpSucceeded -> { + Toast.makeText( + requireContext(), + resources.getQuantityString( + R.plurals.database_dump_success, + effect.databaseCount, + effect.databaseCount, + effect.relativeOutputPath + ), + Toast.LENGTH_LONG + ).show() + } + + SettingsUiEffect.DatabaseDumpFailed -> { + Toast.makeText(requireContext(), R.string.database_dump_failed, Toast.LENGTH_LONG) + .show() + } + } + } + /** * Checkbox listener to prevent all checkboxes from getting unchecked. */ @@ -200,8 +253,7 @@ class SettingsFragment : PreferenceFragmentCompat() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) _binding = CustomPreferenceBinding.bind(view) - - super.onViewCreated(view, savedInstanceState) + observeSettingsEffects() val autoInstallUpdate = fetchCheckboxPreference(R.string.auto_install_enabled) val onlyUnmeteredNetwork = fetchCheckboxPreference(R.string.only_unmetered_network) diff --git a/app/src/main/java/foundation/e/apps/ui/settings/SettingsViewModel.kt b/app/src/main/java/foundation/e/apps/ui/settings/SettingsViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..51693513b075137ffc74e3be6e54dec45d0079c0 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/ui/settings/SettingsViewModel.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.ui.settings + +import android.os.SystemClock +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import foundation.e.apps.domain.debug.ExportDatabaseDumpUseCase +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.io.IOException +import javax.inject.Inject + +@HiltViewModel +class SettingsViewModel @Inject constructor( + private val exportDatabaseDumpUseCase: ExportDatabaseDumpUseCase, +) : ViewModel() { + + private val effectChannel = Channel(capacity = Channel.BUFFERED) + val effect = effectChannel.receiveAsFlow() + + private var versionInfoTapCount = 0 + private var lastVersionInfoTapAt = 0L + private var isDatabaseDumpInProgress = false + + fun onVersionInfoTapped() { + if (isDatabaseDumpInProgress) { + viewModelScope.launch { + effectChannel.send(SettingsUiEffect.DatabaseDumpInProgress) + } + return + } + + val now = SystemClock.elapsedRealtime() + if (now - lastVersionInfoTapAt > VERSION_INFO_TAP_TIMEOUT_MS) { + versionInfoTapCount = 0 + } + + lastVersionInfoTapAt = now + versionInfoTapCount += 1 + + val remainingTaps = DATABASE_DUMP_TRIGGER_TAP_COUNT - versionInfoTapCount + if (remainingTaps > 0) { + return + } + + versionInfoTapCount = 0 + lastVersionInfoTapAt = 0L + exportDatabases() + } + + private fun exportDatabases() { + isDatabaseDumpInProgress = true + viewModelScope.launch { + effectChannel.send(SettingsUiEffect.DatabaseDumpStarted) + try { + val result = withContext(Dispatchers.IO) { + exportDatabaseDumpUseCase() + } + effectChannel.send( + SettingsUiEffect.DatabaseDumpSucceeded( + databaseCount = result.databaseCount, + relativeOutputPath = result.relativeOutputPath, + ) + ) + } catch (exception: CancellationException) { + throw exception + } catch (exception: IOException) { + Timber.e(exception, "Failed to export databases from version info tap") + effectChannel.send(SettingsUiEffect.DatabaseDumpFailed) + } catch (exception: SecurityException) { + Timber.e(exception, "Failed to export databases from version info tap") + effectChannel.send(SettingsUiEffect.DatabaseDumpFailed) + } finally { + isDatabaseDumpInProgress = false + } + } + } + + private companion object { + private const val DATABASE_DUMP_TRIGGER_TAP_COUNT = 7 + private const val VERSION_INFO_TAP_TIMEOUT_MS = 3_000L + } +} + +sealed interface SettingsUiEffect { + data object DatabaseDumpStarted : SettingsUiEffect + data object DatabaseDumpInProgress : SettingsUiEffect + data class DatabaseDumpSucceeded( + val databaseCount: Int, + val relativeOutputPath: String, + ) : SettingsUiEffect + + data object DatabaseDumpFailed : SettingsUiEffect +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7cb3f8fef116b49009311ca329f323cd6a5265d2..303476229360fe818b95d305f4aca898d334040a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -205,6 +205,16 @@ Some network issue is preventing fetching all applications. Open Settings More info + + + Exporting app databases to Downloads... + + Exported %1$d database to %2$s + Exported %1$d databases to %2$s + + Database export failed. + Database export is already running. + The anonymous account you\'re currently using is unavailable. Please refresh the session to get another one. Account unavailable diff --git a/data/src/main/java/foundation/e/apps/data/debug/DatabaseDumpArchiveWriter.kt b/data/src/main/java/foundation/e/apps/data/debug/DatabaseDumpArchiveWriter.kt new file mode 100644 index 0000000000000000000000000000000000000000..955a85b865893fb2d2d92d915f4ec36c72a18fc4 --- /dev/null +++ b/data/src/main/java/foundation/e/apps/data/debug/DatabaseDumpArchiveWriter.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.data.debug + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import java.io.File +import java.io.OutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DatabaseDumpArchiveWriter @Inject constructor( + @ApplicationContext private val context: Context, +) { + + fun getDatabaseNames(): List { + return filterPrimaryDatabaseNames(context.databaseList().toList()) + } + + fun filterPrimaryDatabaseNames(databaseNames: List): List { + return databaseNames + .filterNot(::isSidecarDatabaseFile) + .sorted() + } + + fun writeArchive(outputStream: OutputStream, databaseNames: List): Int { + return ZipOutputStream(outputStream.buffered()).use { zipOutputStream -> + databaseNames.sumOf { databaseName -> + writeDatabaseFiles(zipOutputStream, databaseName) + } + } + } + + private fun writeDatabaseFiles(zipOutputStream: ZipOutputStream, databaseName: String): Int { + val databaseFile = context.getDatabasePath(databaseName) + if (!databaseFile.exists()) { + return 0 + } + + return DATABASE_FILE_SUFFIXES.count { suffix -> + val sourceFile = File("${databaseFile.absolutePath}$suffix") + if (!sourceFile.exists()) { + return@count false + } + + sourceFile.inputStream().buffered().use { inputStream -> + zipOutputStream.putNextEntry(ZipEntry(sourceFile.name)) + inputStream.copyTo(zipOutputStream) + zipOutputStream.closeEntry() + } + true + } + } + + private fun isSidecarDatabaseFile(databaseName: String): Boolean { + return DATABASE_FILE_SUFFIXES_WITHOUT_BASE.any(databaseName::endsWith) + } + + private companion object { + private val DATABASE_FILE_SUFFIXES = listOf("", "-wal", "-shm", "-journal") + private val DATABASE_FILE_SUFFIXES_WITHOUT_BASE = DATABASE_FILE_SUFFIXES.drop(1) + } +} diff --git a/data/src/main/java/foundation/e/apps/data/debug/DatabaseDumpRepositoryImpl.kt b/data/src/main/java/foundation/e/apps/data/debug/DatabaseDumpRepositoryImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..78bc0dda3887dd5386458be98abb9a308ddfe213 --- /dev/null +++ b/data/src/main/java/foundation/e/apps/data/debug/DatabaseDumpRepositoryImpl.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.data.debug + +import android.content.ContentValues +import android.content.Context +import android.os.Environment +import android.provider.MediaStore +import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.domain.debug.DatabaseDumpExportResult +import foundation.e.apps.domain.debug.DatabaseDumpRepository +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DatabaseDumpRepositoryImpl @Inject constructor( + @ApplicationContext private val context: Context, + private val archiveWriter: DatabaseDumpArchiveWriter, +) : DatabaseDumpRepository { + + override fun export(): DatabaseDumpExportResult { + val databaseNames = archiveWriter.getDatabaseNames() + if (databaseNames.isEmpty()) { + throw IOException("No databases found to export.") + } + + val archiveName = createArchiveName() + val resolver = context.contentResolver + val values = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, archiveName) + put(MediaStore.MediaColumns.MIME_TYPE, ZIP_MIME_TYPE) + put(MediaStore.MediaColumns.RELATIVE_PATH, EXPORT_RELATIVE_DIRECTORY) + put(MediaStore.MediaColumns.IS_PENDING, 1) + } + + val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values) + ?: throw IOException("Failed to create database dump archive.") + + try { + val exportedFileCount = resolver.openOutputStream(uri)?.use { outputStream -> + archiveWriter.writeArchive(outputStream, databaseNames) + } ?: throw IOException("Failed to open database dump archive.") + + if (exportedFileCount == 0) { + throw IOException("No database files were copied.") + } + + ContentValues().apply { + put(MediaStore.MediaColumns.IS_PENDING, 0) + }.also { pendingClearedValues -> + resolver.update(uri, pendingClearedValues, null, null) + } + + return DatabaseDumpExportResult( + databaseCount = databaseNames.size, + exportedFileCount = exportedFileCount, + relativeOutputPath = "$EXPORT_RELATIVE_DIRECTORY/$archiveName" + ) + } catch (throwable: Throwable) { + resolver.delete(uri, null, null) + throw throwable + } + } + + private fun createArchiveName(): String { + val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) + return "app_lounge_database_dump_$timestamp.zip" + } + + private companion object { + private const val ZIP_MIME_TYPE = "application/zip" + private val EXPORT_RELATIVE_DIRECTORY = "${Environment.DIRECTORY_DOWNLOADS}/AppLounge" + } +} diff --git a/data/src/test/java/foundation/e/apps/data/debug/DatabaseDumpArchiveWriterTest.kt b/data/src/test/java/foundation/e/apps/data/debug/DatabaseDumpArchiveWriterTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..9636faf06f8674c6763ccbfcdb9854354429fb4f --- /dev/null +++ b/data/src/test/java/foundation/e/apps/data/debug/DatabaseDumpArchiveWriterTest.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.data.debug + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth.assertThat +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.util.zip.ZipInputStream +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [30]) +class DatabaseDumpArchiveWriterTest { + + private lateinit var context: Context + private lateinit var archiveWriter: DatabaseDumpArchiveWriter + private lateinit var databaseDirectory: File + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + archiveWriter = DatabaseDumpArchiveWriter(context) + databaseDirectory = context.getDatabasePath("placeholder").parentFile!! + databaseDirectory.mkdirs() + databaseDirectory.listFiles()?.forEach(File::delete) + } + + @Test + fun filterPrimaryDatabaseNames_excludesSidecarsAndSortsNames() { + val filteredNames = archiveWriter.filterPrimaryDatabaseNames( + listOf( + "fused_database-wal", + "App_Lounge", + "fused_database", + "App_Lounge-shm", + "fused_database-journal", + ) + ) + + assertThat(filteredNames).containsExactly("App_Lounge", "fused_database").inOrder() + } + + @Test + fun writeArchive_includesBaseAndSidecarFilesForPrimaryDatabases() { + createDatabaseFile("App_Lounge", "main") + createDatabaseFile("App_Lounge-wal", "wal") + createDatabaseFile("App_Lounge-shm", "shm") + createDatabaseFile("fused_database", "fused") + createDatabaseFile("fused_database-journal", "journal") + + val outputStream = ByteArrayOutputStream() + val exportedFileCount = archiveWriter.writeArchive( + outputStream = outputStream, + databaseNames = listOf("App_Lounge", "fused_database") + ) + + val archiveContents = readZipContents(outputStream.toByteArray()) + + assertThat(exportedFileCount).isEqualTo(5) + assertThat(archiveContents.keys).containsExactly( + "App_Lounge", + "App_Lounge-wal", + "App_Lounge-shm", + "fused_database", + "fused_database-journal", + ) + assertThat(archiveContents["App_Lounge"]).isEqualTo("main") + assertThat(archiveContents["App_Lounge-wal"]).isEqualTo("wal") + assertThat(archiveContents["App_Lounge-shm"]).isEqualTo("shm") + assertThat(archiveContents["fused_database"]).isEqualTo("fused") + assertThat(archiveContents["fused_database-journal"]).isEqualTo("journal") + } + + private fun createDatabaseFile(name: String, contents: String) { + File(databaseDirectory, name).writeText(contents) + } + + private fun readZipContents(bytes: ByteArray): Map { + val contents = linkedMapOf() + ZipInputStream(ByteArrayInputStream(bytes)).use { zipInputStream -> + var entry = zipInputStream.nextEntry + while (entry != null) { + contents[entry.name] = zipInputStream.readBytes().decodeToString() + zipInputStream.closeEntry() + entry = zipInputStream.nextEntry + } + } + return contents + } +} diff --git a/domain/src/main/kotlin/foundation/e/apps/domain/debug/DatabaseDumpExportResult.kt b/domain/src/main/kotlin/foundation/e/apps/domain/debug/DatabaseDumpExportResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..701c132fd3b79b1af2ff49852f3679604255f2e2 --- /dev/null +++ b/domain/src/main/kotlin/foundation/e/apps/domain/debug/DatabaseDumpExportResult.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.domain.debug + +data class DatabaseDumpExportResult( + val databaseCount: Int, + val exportedFileCount: Int, + val relativeOutputPath: String, +) diff --git a/domain/src/main/kotlin/foundation/e/apps/domain/debug/DatabaseDumpRepository.kt b/domain/src/main/kotlin/foundation/e/apps/domain/debug/DatabaseDumpRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..52c4ef1f5ff48999568ae9972768a130c138d321 --- /dev/null +++ b/domain/src/main/kotlin/foundation/e/apps/domain/debug/DatabaseDumpRepository.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.domain.debug + +interface DatabaseDumpRepository { + fun export(): DatabaseDumpExportResult +} diff --git a/domain/src/main/kotlin/foundation/e/apps/domain/debug/ExportDatabaseDumpUseCase.kt b/domain/src/main/kotlin/foundation/e/apps/domain/debug/ExportDatabaseDumpUseCase.kt new file mode 100644 index 0000000000000000000000000000000000000000..13145a49d1c2aa4370f9189285571bc2285cc89a --- /dev/null +++ b/domain/src/main/kotlin/foundation/e/apps/domain/debug/ExportDatabaseDumpUseCase.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.domain.debug + +import javax.inject.Inject + +class ExportDatabaseDumpUseCase @Inject constructor( + private val databaseDumpRepository: DatabaseDumpRepository, +) { + operator fun invoke(): DatabaseDumpExportResult { + return databaseDumpRepository.export() + } +}