From 5768d0235927d7111894e49fe6b70e923ead9635 Mon Sep 17 00:00:00 2001 From: udenr <98773052+udenr@users.noreply.github.com> Date: Fri, 6 Jun 2025 16:29:45 +0200 Subject: [PATCH 1/5] Remove link to Google Play from `README.md` --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 1b142d6..cbc7026 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,6 @@ This is the privacy friendly backup application that enables users to create and [Get it on F-Droid](https://f-droid.org/packages/org.secuso.privacyfriendlybackup/) -[Get it on Google Play](https://play.google.com/store/apps/details?id=org.secuso.privacyfriendlybackup) ## Integrated PFAs These apps support backups with the Privacy Friendly Backup app. From 88a446e94bf1d516459747afa07b866d3c426022 Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Thu, 16 Oct 2025 21:40:33 +0200 Subject: [PATCH 2/5] [chore] updates AGP and kotlin. --- app/build.gradle | 3 ++- .../privacyfriendlybackup/data/cloud/WebserviceProvider.kt | 5 +---- build.gradle | 6 +++--- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 0ee5bf3..90435e4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -107,6 +107,7 @@ dependencies { implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.constraintlayout:constraintlayout:2.2.0' + implementation 'androidx.annotation:annotation:1.9.1' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.2.1' @@ -123,7 +124,7 @@ dependencies { testImplementation "androidx.work:work-testing:$work_version" // Room Database - def roomVersion = "2.6.1" + def roomVersion = "2.7.0" implementation "androidx.room:room-runtime:$roomVersion" annotationProcessor "androidx.room:room-compiler:$roomVersion" ksp "androidx.room:room-compiler:$roomVersion" diff --git a/app/src/main/java/org/secuso/privacyfriendlybackup/data/cloud/WebserviceProvider.kt b/app/src/main/java/org/secuso/privacyfriendlybackup/data/cloud/WebserviceProvider.kt index 8f8d26e..9f84b37 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybackup/data/cloud/WebserviceProvider.kt +++ b/app/src/main/java/org/secuso/privacyfriendlybackup/data/cloud/WebserviceProvider.kt @@ -3,7 +3,6 @@ package org.secuso.privacyfriendlybackup.data.cloud import android.content.Context import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.secuso.privacyfriendlybackup.api.util.copyInputStreamToFile import org.secuso.privacyfriendlybackup.api.util.hash import org.secuso.privacyfriendlybackup.api.util.toHex import org.secuso.privacyfriendlybackup.data.BackupDataStorageRepository @@ -13,8 +12,6 @@ import org.secuso.privacyfriendlybackup.data.internal.InternalBackupDataStoreHel import org.secuso.privacyfriendlybackup.data.room.BackupDatabase import org.secuso.privacyfriendlybackup.data.room.model.StoredBackupMetaData import org.secuso.privacyfriendlybackup.data.room.model.enums.StorageType -import org.secuso.privacyfriendlybackup.util.BackupDataUtil -import java.io.ByteArrayOutputStream import java.io.File import java.util.* @@ -74,7 +71,7 @@ class WebserviceProvider { return withContext(Dispatchers.IO) { val files = File(context.getExternalFilesDir(null), ExternalBackupDataStoreHelper.BACKUP_DIR - ).listFiles { _, name -> name.toLowerCase(Locale.ENGLISH).endsWith(".backup") } + ).listFiles { _, name -> name.lowercase(Locale.ENGLISH).endsWith(".backup") } files?.map { it.name } ?: emptyList() } } diff --git a/build.gradle b/build.gradle index bd077c6..3e5f302 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,13 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.9.22" + ext.kotlin_version = "2.2.20" repositories { google() mavenCentral() maven { url 'https://jitpack.io' } } dependencies { - classpath 'com.android.tools.build:gradle:8.7.3' + classpath 'com.android.tools.build:gradle:8.10.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong @@ -16,7 +16,7 @@ buildscript { } plugins { - id 'com.google.devtools.ksp' version "$kotlin_version-1.0.17" apply false + id 'com.google.devtools.ksp' version "$kotlin_version-2.0.4" apply false } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index da6416a..120d9fd 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Jan 23 03:28:08 CET 2021 +#Sun Oct 12 18:03:10 CEST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip From 451bd8a5eb7b57606d2774cd7356568e63b38ead Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Thu, 16 Oct 2025 21:41:07 +0200 Subject: [PATCH 3/5] [feat] Adds storage type Internal. Adds option to select storage location. --- .../data/BackupDataStorageRepository.kt | 30 +++++-- .../external/ExternalBackupDataStoreHelper.kt | 2 +- .../internal/InternalBackupDataStoreHelper.kt | 81 ++++++++++++++++++- .../data/room/model/enums/StorageType.kt | 3 +- .../ApplicationOverviewViewModel.kt | 2 +- .../ui/backup/FilterableBackupAdapter.kt | 2 +- .../ui/main/MainActivity.kt | 5 +- .../ui/settings/SettingsFragment.kt | 3 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/pref_general.xml | 8 +- 11 files changed, 121 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/secuso/privacyfriendlybackup/data/BackupDataStorageRepository.kt b/app/src/main/java/org/secuso/privacyfriendlybackup/data/BackupDataStorageRepository.kt index 856a7eb..5a4c3d8 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybackup/data/BackupDataStorageRepository.kt +++ b/app/src/main/java/org/secuso/privacyfriendlybackup/data/BackupDataStorageRepository.kt @@ -4,18 +4,13 @@ import android.content.Context import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData -import androidx.room.ColumnInfo -import androidx.room.PrimaryKey import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.secuso.privacyfriendlybackup.data.room.BackupDatabase import org.secuso.privacyfriendlybackup.data.cloud.WebserviceProvider import org.secuso.privacyfriendlybackup.data.external.ExternalBackupDataStoreHelper -import org.secuso.privacyfriendlybackup.data.room.model.BackupJob -import org.secuso.privacyfriendlybackup.data.room.model.enums.BackupJobAction +import org.secuso.privacyfriendlybackup.data.internal.InternalBackupDataStoreHelper import org.secuso.privacyfriendlybackup.data.room.model.enums.StorageType import java.util.* @@ -64,6 +59,7 @@ class BackupDataStorageRepository private constructor( suspend fun storeFile(context: Context, packageName: String, dataId : Long, storageType: StorageType = StorageType.EXTERNAL) { when(storageType) { StorageType.EXTERNAL -> ExternalBackupDataStoreHelper.storeData(context, packageName, dataId) + StorageType.INTERNAL -> InternalBackupDataStoreHelper.storeData(context, packageName, dataId) StorageType.CLOUD -> webserviceProvider.storeData(context, packageName, dataId) } } @@ -83,6 +79,9 @@ class BackupDataStorageRepository private constructor( StorageType.EXTERNAL -> { data.postValue(ExternalBackupDataStoreHelper.getData(context,metadata)) } + StorageType.INTERNAL -> { + data.postValue(InternalBackupDataStoreHelper.getData(context,metadata)) + } StorageType.CLOUD -> { data.postValue( TODO() ) } @@ -99,6 +98,7 @@ class BackupDataStorageRepository private constructor( return when (metadata.storageService) { StorageType.EXTERNAL -> ExternalBackupDataStoreHelper.getData(context, metadata) + StorageType.INTERNAL -> InternalBackupDataStoreHelper.getData(context, metadata) StorageType.CLOUD -> webserviceProvider.getData(context, metadata) } } @@ -112,6 +112,7 @@ class BackupDataStorageRepository private constructor( runBlocking { val externalFilenames = ExternalBackupDataStoreHelper.listAvailableData(context) + val internalFilenames = InternalBackupDataStoreHelper.listAvailableData(context) for(meta in metalist) { when(meta.storageService) { StorageType.EXTERNAL -> { @@ -128,6 +129,20 @@ class BackupDataStorageRepository private constructor( ) result.add(backupDataInfo) } + StorageType.INTERNAL -> { + val available = internalFilenames.contains(meta.filename) + val backupDataInfo = BackupData( + id = meta._id, + filename = meta.filename, + packageName = meta.packageName, + timestamp = meta.timestamp, + data = null, + encrypted = meta.encrypted, + storageType = meta.storageService, + available = available + ) + result.add(backupDataInfo) + } StorageType.CLOUD -> { TODO() } @@ -149,6 +164,9 @@ class BackupDataStorageRepository private constructor( StorageType.EXTERNAL -> { ExternalBackupDataStoreHelper.deleteData(context, metaData) } + StorageType.INTERNAL -> { + InternalBackupDataStoreHelper.deleteData(context, metaData) + } StorageType.CLOUD -> { webserviceProvider.deleteData(context, metaData) } diff --git a/app/src/main/java/org/secuso/privacyfriendlybackup/data/external/ExternalBackupDataStoreHelper.kt b/app/src/main/java/org/secuso/privacyfriendlybackup/data/external/ExternalBackupDataStoreHelper.kt index f667809..ef8a401 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybackup/data/external/ExternalBackupDataStoreHelper.kt +++ b/app/src/main/java/org/secuso/privacyfriendlybackup/data/external/ExternalBackupDataStoreHelper.kt @@ -106,7 +106,7 @@ object ExternalBackupDataStoreHelper { suspend fun listAvailableData(context: Context) : List { return withContext(Dispatchers.IO) { - val files = File(context.getExternalFilesDir(null), BACKUP_DIR).listFiles { _, name -> name.toLowerCase(Locale.ENGLISH).endsWith(".backup") } + val files = File(context.getExternalFilesDir(null), BACKUP_DIR).listFiles { _, name -> name.lowercase(Locale.ENGLISH).endsWith(".backup") } files?.map { it.name } ?: emptyList() } } diff --git a/app/src/main/java/org/secuso/privacyfriendlybackup/data/internal/InternalBackupDataStoreHelper.kt b/app/src/main/java/org/secuso/privacyfriendlybackup/data/internal/InternalBackupDataStoreHelper.kt index df3fce4..5a0dbad 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybackup/data/internal/InternalBackupDataStoreHelper.kt +++ b/app/src/main/java/org/secuso/privacyfriendlybackup/data/internal/InternalBackupDataStoreHelper.kt @@ -1,13 +1,20 @@ package org.secuso.privacyfriendlybackup.data.internal import android.content.Context -import android.text.TextUtils import android.util.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.secuso.privacyfriendlybackup.api.util.copyInputStreamToFile +import org.secuso.privacyfriendlybackup.api.util.hash +import org.secuso.privacyfriendlybackup.api.util.toHex +import org.secuso.privacyfriendlybackup.data.BackupDataStorageRepository import org.secuso.privacyfriendlybackup.data.room.BackupDatabase import org.secuso.privacyfriendlybackup.data.room.model.enums.BackupJobAction import org.secuso.privacyfriendlybackup.data.room.model.InternalBackupData +import org.secuso.privacyfriendlybackup.data.room.model.StoredBackupMetaData +import org.secuso.privacyfriendlybackup.data.room.model.enums.StorageType import org.secuso.privacyfriendlybackup.util.BackupDataUtil.getFileName +import java.io.ByteArrayInputStream import java.io.File import java.io.IOException import java.io.InputStream @@ -17,6 +24,7 @@ object InternalBackupDataStoreHelper { const val TAG = "PFA Internal" const val BACKUP_DIR = "tempData" + const val INTERNAL_BACKUP_DIR = "backupData" suspend fun storeBackupData(context: Context, packageName: String, inputStream: InputStream, date: Date, encrypted: Boolean = false) : Long { val dataId = storeData(context, packageName, inputStream, date, encrypted) @@ -42,6 +50,42 @@ object InternalBackupDataStoreHelper { return dataId } + suspend fun storeData(context: Context, packageName: String, dataId: Long) : Long { + return withContext(Dispatchers.IO) { + val path = File(context.filesDir, INTERNAL_BACKUP_DIR) + path.mkdirs() + + val date = Date() + + val (inputStream, data) = getInternalData(context, dataId) + val fileName = getFileName(date, packageName, data?.encrypted == true) + val file = File(path, fileName) + + Log.d(TAG, file.toString()) + + val dataBytes = inputStream?.use { + return@use inputStream.readBytes() + } + file.copyInputStreamToFile(ByteArrayInputStream(dataBytes)) + + if (data != null) { + val hash = dataBytes!!.hash("SHA-1").toHex() + BackupDatabase.getInstance(context).backupMetaDataDao().insert( + StoredBackupMetaData( + packageName = data.packageName, + timestamp = date, + storageService = StorageType.INTERNAL, + filename = fileName, + encrypted = data.encrypted, + hash = hash + ) + ) + } else { + -1L + } + } + } + suspend fun storeData(context: Context, packageName: String, inputStream: InputStream, date: Date, encrypted : Boolean = false) : Long { val path = File(context.filesDir, BACKUP_DIR) path.mkdirs() @@ -61,6 +105,24 @@ object InternalBackupDataStoreHelper { return BackupDatabase.getInstance(context).internalBackupDataDao().insert(data) } + suspend fun getData(context: Context, metadata : StoredBackupMetaData) : BackupDataStorageRepository.BackupData? { + return withContext(Dispatchers.IO) { + val path = File(context.filesDir, INTERNAL_BACKUP_DIR) + val file = File(path, metadata.filename) + + return@withContext BackupDataStorageRepository.BackupData( + metadata._id, + metadata.filename, + metadata.packageName, + metadata.timestamp, + file.inputStream().readBytes(), + metadata.encrypted, + StorageType.INTERNAL, + true + ) + } + } + suspend fun getInternalData(context: Context, dataId: Long): Pair { val data = BackupDatabase.getInstance(context).internalBackupDataDao().getById(dataId) ?: return Pair(null, null) @@ -117,4 +179,21 @@ object InternalBackupDataStoreHelper { } } + suspend fun deleteData(context: Context, metadata : StoredBackupMetaData) { + withContext(Dispatchers.IO) { + val path = File(context.filesDir, INTERNAL_BACKUP_DIR) + val file = File(path, metadata.filename) + file.delete() + } + } + + suspend fun listAvailableData(context: Context) : List { + return withContext(Dispatchers.IO) { + val files = File(context.filesDir, INTERNAL_BACKUP_DIR).listFiles { _, name -> + name.lowercase(Locale.ENGLISH).endsWith(".backup") + } + files?.map { it.name } ?: emptyList() + } + } + } \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlybackup/data/room/model/enums/StorageType.kt b/app/src/main/java/org/secuso/privacyfriendlybackup/data/room/model/enums/StorageType.kt index 0813734..5d17d42 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybackup/data/room/model/enums/StorageType.kt +++ b/app/src/main/java/org/secuso/privacyfriendlybackup/data/room/model/enums/StorageType.kt @@ -7,10 +7,11 @@ enum class StorageType( @StringRes val nameResId : Int ) { EXTERNAL(R.string.storage_type_external), + INTERNAL(R.string.storage_type_internal), CLOUD(R.string.storage_type_drive); companion object { @JvmStatic - fun getStorageOptions() = listOf(EXTERNAL) + fun getStorageOptions() = listOf(EXTERNAL, INTERNAL) } } \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlybackup/ui/application/ApplicationOverviewViewModel.kt b/app/src/main/java/org/secuso/privacyfriendlybackup/ui/application/ApplicationOverviewViewModel.kt index 7a6c73d..e8c5bae 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybackup/ui/application/ApplicationOverviewViewModel.kt +++ b/app/src/main/java/org/secuso/privacyfriendlybackup/ui/application/ApplicationOverviewViewModel.kt @@ -79,7 +79,7 @@ class ApplicationOverviewViewModel(app : Application) : AndroidViewModel(app) { fun createBackupForPackage(packageName: String) { viewModelScope.launch { val jobManager = BackupJobManager.getInstance(getApplication()) - val storageName = PreferenceManager.getDefaultSharedPreferences(getApplication()).getString(PreferenceKeys.PREF_STORAGE_TYPE, StorageType.EXTERNAL.name) + val storageName = PreferenceManager.getDefaultSharedPreferences(getApplication()).getString(PreferenceKeys.PREF_STORAGE_TYPE, StorageType.INTERNAL.name) val storageType = StorageType.valueOf(storageName!!) jobManager.createBackupJobChain(packageName, storageType) } diff --git a/app/src/main/java/org/secuso/privacyfriendlybackup/ui/backup/FilterableBackupAdapter.kt b/app/src/main/java/org/secuso/privacyfriendlybackup/ui/backup/FilterableBackupAdapter.kt index 3defa88..cffc052 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybackup/ui/backup/FilterableBackupAdapter.kt +++ b/app/src/main/java/org/secuso/privacyfriendlybackup/ui/backup/FilterableBackupAdapter.kt @@ -124,7 +124,7 @@ class FilterableBackupAdapter(val context : Context, adapterCallback : ManageLis } val icon = when(data.storageType) { - StorageType.EXTERNAL -> if(data.encrypted) R.drawable.ic_baseline_phonelink_lock_24 else R.drawable.ic_baseline_smartphone_24 + StorageType.EXTERNAL, StorageType.INTERNAL -> if(data.encrypted) R.drawable.ic_baseline_phonelink_lock_24 else R.drawable.ic_baseline_smartphone_24 StorageType.CLOUD -> if(data.encrypted) R.drawable.ic_cloud_24 else R.drawable.ic_cloud_24 } diff --git a/app/src/main/java/org/secuso/privacyfriendlybackup/ui/main/MainActivity.kt b/app/src/main/java/org/secuso/privacyfriendlybackup/ui/main/MainActivity.kt index acf9753..fb87906 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybackup/ui/main/MainActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlybackup/ui/main/MainActivity.kt @@ -26,6 +26,7 @@ import org.secuso.privacyfriendlybackup.ui.backup.BackupOverviewFragment import org.secuso.privacyfriendlybackup.ui.common.DisplayMenuItemActivity import org.secuso.privacyfriendlybackup.ui.encryption.EncryptionSettingsFragment import org.secuso.privacyfriendlybackup.ui.help.HelpFragment +import org.secuso.privacyfriendlybackup.ui.settings.SettingsFragment /** * An activity representing a list of Pings. This activity @@ -46,7 +47,7 @@ class MainActivity : AppCompatActivity() { MENU_MAIN_BACKUP_OVERVIEW(R.drawable.ic_backup_24, R.string.menu_main_backup, BackupOverviewFragment::class.java, DisplayMenuItemActivity::class.java), MENU_MAIN_APPS(R.drawable.ic_apps_24, R.string.menu_main_apps, ApplicationOverviewFragment::class.java, DisplayMenuItemActivity::class.java), MENU_MAIN_ENCRYPTION(R.drawable.ic_encryption_24, R.string.menu_main_encryption, EncryptionSettingsFragment::class.java, DisplayMenuItemActivity::class.java), - //MENU_MAIN_SETTINGS(R.drawable.ic_settings_24, R.string.menu_main_settings, SettingsFragment::class.java, DisplayMenuItemActivity::class.java), + MENU_MAIN_SETTINGS(R.drawable.ic_settings_24, R.string.menu_main_settings, SettingsFragment::class.java, DisplayMenuItemActivity::class.java), MENU_MAIN_HELP(R.drawable.ic_help_outline_24, R.string.menu_main_help, HelpFragment::class.java, DisplayMenuItemActivity::class.java), MENU_MAIN_ABOUT(R.drawable.ic_about_24, R.string.menu_main_about, AboutFragment::class.java, DisplayMenuItemActivity::class.java); @@ -72,7 +73,7 @@ class MainActivity : AppCompatActivity() { MenuItem.MENU_MAIN_APPS, MenuItem.MENU_MAIN_BACKUP_OVERVIEW, MenuItem.MENU_MAIN_ENCRYPTION, - //MenuItem.MENU_MAIN_SETTINGS, + MenuItem.MENU_MAIN_SETTINGS, MenuItem.MENU_MAIN_HELP, MenuItem.MENU_MAIN_ABOUT ) diff --git a/app/src/main/java/org/secuso/privacyfriendlybackup/ui/settings/SettingsFragment.kt b/app/src/main/java/org/secuso/privacyfriendlybackup/ui/settings/SettingsFragment.kt index 2605158..55dfd75 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybackup/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/org/secuso/privacyfriendlybackup/ui/settings/SettingsFragment.kt @@ -93,6 +93,9 @@ class SettingsFragment : PreferenceFragmentCompat() { storageTypePref = findPreference(PREF_STORAGE_TYPE) storageTypePref?.apply { + if (value == null) { + value = StorageType.EXTERNAL.toString() + } entryValues = StorageType.getStorageOptions().map { it.name }.toTypedArray() entries = StorageType.getStorageOptions().map { requireContext().getString(it.nameResId) }.toTypedArray() } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 9583372..4b498ed 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -49,6 +49,7 @@ Speichertyp Automatische Backups Externer Speicher + Interner Speicher Wiederherstellen Fehler: ID ist nicht korrekt Exportieren diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6ed951b..bd90862 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -50,6 +50,7 @@ Storage Type Automatic Backups External Storage + Internal Storage Google Drive Inspect Restore diff --git a/app/src/main/res/xml/pref_general.xml b/app/src/main/res/xml/pref_general.xml index 3cef938..ecb1482 100644 --- a/app/src/main/res/xml/pref_general.xml +++ b/app/src/main/res/xml/pref_general.xml @@ -5,20 +5,20 @@ app:title="@string/pref_category_storage"> - + app:title="Test" /> --> - + \ No newline at end of file From 2b2c4971566481961619d5a6391c39d733fe9102 Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Wed, 4 Feb 2026 21:48:05 +0100 Subject: [PATCH 4/5] Adds option to move all backups from/to external/internal storage. Backups and restoral still works as expected. --- .../data/BackupDataStorageRepository.kt | 1 + .../internal/InternalBackupDataStoreHelper.kt | 34 +++++++++++++++++ .../data/room/dao/BackupMetaDataDao.kt | 4 ++ .../ui/backup/BackupOverviewViewModel.kt | 1 - .../ui/settings/SettingsFragment.kt | 20 ++++++++++ .../ui/settings/SettingsViewmodel.kt | 38 +++++++++++++++++++ .../worker/StoreWorker.kt | 1 - app/src/main/res/values-de/strings.xml | 3 ++ app/src/main/res/values/strings.xml | 3 ++ 9 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/secuso/privacyfriendlybackup/ui/settings/SettingsViewmodel.kt diff --git a/app/src/main/java/org/secuso/privacyfriendlybackup/data/BackupDataStorageRepository.kt b/app/src/main/java/org/secuso/privacyfriendlybackup/data/BackupDataStorageRepository.kt index 5a4c3d8..b7e5dc2 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybackup/data/BackupDataStorageRepository.kt +++ b/app/src/main/java/org/secuso/privacyfriendlybackup/data/BackupDataStorageRepository.kt @@ -51,6 +51,7 @@ class BackupDataStorageRepository private constructor( suspend fun storeFile(context: Context, backupData: BackupData) : Pair { return when(backupData.storageType) { StorageType.EXTERNAL -> ExternalBackupDataStoreHelper.storeData(context, backupData) + StorageType.INTERNAL -> InternalBackupDataStoreHelper.storeData(context, backupData) else -> { false to 0 } //StorageType.CLOUD -> webserviceProvider.storeData(context, packageName, dataId) } diff --git a/app/src/main/java/org/secuso/privacyfriendlybackup/data/internal/InternalBackupDataStoreHelper.kt b/app/src/main/java/org/secuso/privacyfriendlybackup/data/internal/InternalBackupDataStoreHelper.kt index 5a0dbad..c08bfb2 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybackup/data/internal/InternalBackupDataStoreHelper.kt +++ b/app/src/main/java/org/secuso/privacyfriendlybackup/data/internal/InternalBackupDataStoreHelper.kt @@ -86,6 +86,27 @@ object InternalBackupDataStoreHelper { } } + suspend fun storeData(context: Context, data: BackupDataStorageRepository.BackupData) : Pair { + return withContext(Dispatchers.IO) { + val path = File(context.filesDir, INTERNAL_BACKUP_DIR) + path.mkdirs() + val file = File(path, data.filename) + + file.copyInputStreamToFile(ByteArrayInputStream(data.data)) + val hash = data.data!!.hash("SHA-1").toHex() + + val id = BackupDatabase.getInstance(context).backupMetaDataDao().insert(StoredBackupMetaData( + packageName = data.packageName, + timestamp = data.timestamp, + storageService = StorageType.INTERNAL, + filename = data.filename, + encrypted = data.encrypted, + hash = hash + )) + return@withContext true to id + } + } + suspend fun storeData(context: Context, packageName: String, inputStream: InputStream, date: Date, encrypted : Boolean = false) : Long { val path = File(context.filesDir, BACKUP_DIR) path.mkdirs() @@ -127,6 +148,19 @@ object InternalBackupDataStoreHelper { val data = BackupDatabase.getInstance(context).internalBackupDataDao().getById(dataId) ?: return Pair(null, null) +// if(data.packageName != callingPackageName && data.uid == callingUid) { +// Log.d(TAG, "[No Restore Data found.]") +// return null +// } + + val path = File(context.filesDir, BACKUP_DIR) + return File(path, data.file).inputStream() to data + } + + suspend fun getInternalStoredData(context: Context, dataId: Long): Pair { + val data = BackupDatabase.getInstance(context).internalBackupDataDao().getById(dataId) + ?: return Pair(null, null) + // if(data.packageName != callingPackageName && data.uid == callingUid) { // Log.d(TAG, "[No Restore Data found.]") // return null diff --git a/app/src/main/java/org/secuso/privacyfriendlybackup/data/room/dao/BackupMetaDataDao.kt b/app/src/main/java/org/secuso/privacyfriendlybackup/data/room/dao/BackupMetaDataDao.kt index c1b7141..b119060 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybackup/data/room/dao/BackupMetaDataDao.kt +++ b/app/src/main/java/org/secuso/privacyfriendlybackup/data/room/dao/BackupMetaDataDao.kt @@ -3,6 +3,7 @@ package org.secuso.privacyfriendlybackup.data.room.dao import androidx.lifecycle.LiveData import androidx.room.* import org.secuso.privacyfriendlybackup.data.room.model.StoredBackupMetaData +import org.secuso.privacyfriendlybackup.data.room.model.enums.StorageType @Dao interface BackupMetaDataDao { @@ -32,4 +33,7 @@ interface BackupMetaDataDao { @Query("DELETE FROM StoredBackupMetaData WHERE _id IN (:ids)") suspend fun deleteForIds(ids: List) + + @Query("SELECT * FROM StoredBackupMetaData WHERE storageService != :storageType") + suspend fun getAllOfOtherStorageType(storageType: StorageType): List } \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlybackup/ui/backup/BackupOverviewViewModel.kt b/app/src/main/java/org/secuso/privacyfriendlybackup/ui/backup/BackupOverviewViewModel.kt index ae007f4..130a5f5 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybackup/ui/backup/BackupOverviewViewModel.kt +++ b/app/src/main/java/org/secuso/privacyfriendlybackup/ui/backup/BackupOverviewViewModel.kt @@ -2,7 +2,6 @@ package org.secuso.privacyfriendlybackup.ui.backup import android.app.Application import android.net.Uri -import android.widget.Toast import androidx.collection.ArraySet import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData diff --git a/app/src/main/java/org/secuso/privacyfriendlybackup/ui/settings/SettingsFragment.kt b/app/src/main/java/org/secuso/privacyfriendlybackup/ui/settings/SettingsFragment.kt index 55dfd75..60dd808 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybackup/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/org/secuso/privacyfriendlybackup/ui/settings/SettingsFragment.kt @@ -9,11 +9,15 @@ import android.os.Bundle import android.util.Log import androidx.annotation.ColorInt import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.Toolbar import androidx.core.content.ContextCompat import androidx.fragment.app.DialogFragment +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import androidx.preference.* import com.google.android.material.appbar.AppBarLayout +import kotlinx.coroutines.launch import org.secuso.privacyfriendlybackup.R import org.secuso.privacyfriendlybackup.data.room.model.enums.StorageType import org.secuso.privacyfriendlybackup.preference.PreferenceKeys @@ -25,6 +29,8 @@ import org.secuso.privacyfriendlybackup.ui.main.MainActivity class SettingsFragment : PreferenceFragmentCompat() { + private val viewModel by lazy { ViewModelProvider(this)[SettingsViewmodel::class.java] } + companion object { const val DIALOG_FRAGMENT_TAG = "SettingsFragment.DIALOG_FRAGMENT_TAG" const val TAG = "PFA Settings" @@ -98,6 +104,20 @@ class SettingsFragment : PreferenceFragmentCompat() { } entryValues = StorageType.getStorageOptions().map { it.name }.toTypedArray() entries = StorageType.getStorageOptions().map { requireContext().getString(it.nameResId) }.toTypedArray() + onPreferenceChangeListener = Preference.OnPreferenceChangeListener { preference, newValue -> + AlertDialog.Builder(requireContext()).apply { + setTitle(R.string.storage_location_move_all_to_new_title) + setMessage(R.string.storage_location_move_all_to_new_desc) + setNegativeButton(R.string.cancel, null) + setPositiveButton(R.string.move) { _,_ -> + val storageName = PreferenceManager.getDefaultSharedPreferences(requireContext()).getString(PreferenceKeys.PREF_STORAGE_TYPE, StorageType.INTERNAL.name) + val storageType = StorageType.valueOf(storageName!!) + viewModel.moveBackups(storageType) + } + show() + } + return@OnPreferenceChangeListener true + } } val test : SwitchPreference? = findPreference("pref_test") diff --git a/app/src/main/java/org/secuso/privacyfriendlybackup/ui/settings/SettingsViewmodel.kt b/app/src/main/java/org/secuso/privacyfriendlybackup/ui/settings/SettingsViewmodel.kt new file mode 100644 index 0000000..162d8dd --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlybackup/ui/settings/SettingsViewmodel.kt @@ -0,0 +1,38 @@ +package org.secuso.privacyfriendlybackup.ui.settings + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import org.secuso.privacyfriendlybackup.data.external.ExternalBackupDataStoreHelper +import org.secuso.privacyfriendlybackup.data.internal.InternalBackupDataStoreHelper +import org.secuso.privacyfriendlybackup.data.room.BackupDatabase +import org.secuso.privacyfriendlybackup.data.room.model.enums.StorageType + +class SettingsViewmodel(app: Application) : AndroidViewModel(app) { + + fun moveBackups(location: StorageType) { + viewModelScope.launch { + val data = BackupDatabase.getInstance(getApplication()).backupMetaDataDao().getAllOfOtherStorageType(location) + + when (location) { + StorageType.INTERNAL -> { + data.forEach { metadata -> + val backupData = ExternalBackupDataStoreHelper.getData(getApplication(), metadata) + InternalBackupDataStoreHelper.storeData(getApplication(), backupData!!) + ExternalBackupDataStoreHelper.deleteData(getApplication(), metadata) + } + } + StorageType.EXTERNAL -> { + data.forEach { metadata -> + val backupData = InternalBackupDataStoreHelper.getData(getApplication(), metadata) + ExternalBackupDataStoreHelper.storeData(getApplication(), backupData!!) + InternalBackupDataStoreHelper.deleteData(getApplication(), metadata) + } + } + else -> {} + } + } + } + +} diff --git a/app/src/main/java/org/secuso/privacyfriendlybackup/worker/StoreWorker.kt b/app/src/main/java/org/secuso/privacyfriendlybackup/worker/StoreWorker.kt index 491b28c..03986c3 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybackup/worker/StoreWorker.kt +++ b/app/src/main/java/org/secuso/privacyfriendlybackup/worker/StoreWorker.kt @@ -5,7 +5,6 @@ import android.util.Log import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import org.secuso.privacyfriendlybackup.data.BackupDataStorageRepository -import org.secuso.privacyfriendlybackup.data.cloud.WebserviceProvider import org.secuso.privacyfriendlybackup.data.internal.InternalBackupDataStoreHelper import org.secuso.privacyfriendlybackup.data.room.BackupDatabase import org.secuso.privacyfriendlybackup.data.room.model.BackupJob diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4b498ed..532a2dc 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -130,4 +130,7 @@ Um die Verschlüsselung nutzen zu können, müssen Sie eine Kryptoanbieter-Anwendung installieren. Wir empfehlen die OpenKeyChain-Anwendung, da sie quelloffen ist und sowohl im Google Play Store als auch im F-Droid-Store verfügbar ist. Wie viele und welche Apps werden zur Zeit unterstützt? Wir arbeiten stetig daran neue Apps zu unterstützen. Zurzeit wird nur die Notizen-App unterstützt. + Sollen alle Backups zu dem gerade ausgewählten Speicherort verschoben werden? + Verschieben + Alle Backups verschieben \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bd90862..afb39d4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -134,5 +134,8 @@ In order to use the encryption you will need to install a cryptographic provider application. We recommend the OpenKeyChain application because it is open source and available in both the Google Play Store as well as the f-Droid store. How many and which apps are currently supported? Currently only the Notes App is supported but we are working on adding more applications to this list. + Move all backups to new location + Do you want to move all backups made to the just selected storage location? + Relocate \ No newline at end of file From 8841967588af8e66ea0863abd324408963c4c0d0 Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Fri, 6 Mar 2026 09:59:45 +0100 Subject: [PATCH 5/5] [feat] Correct default storage type. Adds warning to only use external iff encryption enabled. If the app was upgraded, so there were backups present, then external is kept as default. Otherwise internal is used. --- .../BackupApplication.kt | 29 +++++++++++++++++++ .../data/room/dao/BackupMetaDataDao.kt | 3 ++ app/src/main/res/values-de/strings.xml | 6 ++++ app/src/main/res/values/strings.xml | 6 ++++ app/src/main/res/xml/pref_general.xml | 3 +- 5 files changed, 45 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/secuso/privacyfriendlybackup/BackupApplication.kt b/app/src/main/java/org/secuso/privacyfriendlybackup/BackupApplication.kt index b86df29..d3454bc 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybackup/BackupApplication.kt +++ b/app/src/main/java/org/secuso/privacyfriendlybackup/BackupApplication.kt @@ -5,12 +5,19 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.os.Build import android.util.Log +import androidx.core.content.edit +import androidx.lifecycle.ProcessLifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.preference.PreferenceManager import androidx.work.BackoffPolicy import androidx.work.Configuration import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import androidx.work.WorkRequest +import kotlinx.coroutines.launch +import org.secuso.privacyfriendlybackup.data.room.BackupDatabase +import org.secuso.privacyfriendlybackup.data.room.model.enums.StorageType import org.secuso.privacyfriendlybackup.worker.BackupJobManagerWorker import java.util.concurrent.TimeUnit @@ -25,6 +32,28 @@ class BackupApplication : Application(), Configuration.Provider { override fun onCreate() { super.onCreate() + if (applicationContext != null) { + with(ProcessLifecycleOwner.get()) { + lifecycleScope.launch { + PreferenceManager.getDefaultSharedPreferences(applicationContext).apply{ + val current = getString("pref_storage_type", null) + if (current == null) { + val total = BackupDatabase.getInstance(applicationContext) + .backupMetaDataDao() + .getTotal() + edit(commit = true) { + putString("pref_storage_type", if (total > 0) { + StorageType.EXTERNAL.toString() + } else { + StorageType.INTERNAL.toString() + }) + } + } + } + } + } + } + createNotificationChannel() schedulePeriodicWork() } diff --git a/app/src/main/java/org/secuso/privacyfriendlybackup/data/room/dao/BackupMetaDataDao.kt b/app/src/main/java/org/secuso/privacyfriendlybackup/data/room/dao/BackupMetaDataDao.kt index b119060..76aa676 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybackup/data/room/dao/BackupMetaDataDao.kt +++ b/app/src/main/java/org/secuso/privacyfriendlybackup/data/room/dao/BackupMetaDataDao.kt @@ -36,4 +36,7 @@ interface BackupMetaDataDao { @Query("SELECT * FROM StoredBackupMetaData WHERE storageService != :storageType") suspend fun getAllOfOtherStorageType(storageType: StorageType): List + + @Query("SELECT COUNT(*) FROM StoredBackupMetaData") + suspend fun getTotal(): Long } \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 532a2dc..b36e35e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -47,6 +47,12 @@ Google Drive Speichereinstellungen Speichertyp + +Wählen Sie explizit aus, an welcher Stelle die Backup-Daten und -Metadaten gespeichert werden soll.\n +Achtung: Der externe Speicher ermöglicht anderen Apps, Diensten und Nutzern diese auszulesen oder zu verändern. Nutzen Sie diese Option nur, wenn die Verschlüsselung der Backups eingerichtet ist und verwendet wird. \n +\n +Aktueller Speicherort: %s + Automatische Backups Externer Speicher Interner Speicher diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index afb39d4..51d72af 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -48,6 +48,12 @@ Restore data Storage Settings Storage Type + +Specify the storage location used to store the backup data and metadata. \n +Warning: Using the external storage makes the backup data available to other apps and users. Only use the external storage if encryption is properly setup and enabled. \n +\n +Current storage location: %s + Automatic Backups External Storage Internal Storage diff --git a/app/src/main/res/xml/pref_general.xml b/app/src/main/res/xml/pref_general.xml index ecb1482..55404cd 100644 --- a/app/src/main/res/xml/pref_general.xml +++ b/app/src/main/res/xml/pref_general.xml @@ -5,10 +5,9 @@ app:title="@string/pref_category_storage"> + app:summary="@string/pref_storage_type_summary" />