Skip to content

Commit

Permalink
Merge pull request #823 from grote/android14-backports
Browse files Browse the repository at this point in the history
Android14 backport of 15-5.2
  • Loading branch information
chirayudesai authored Dec 27, 2024
2 parents 1eb2a73 + 419b65f commit 1229cea
Show file tree
Hide file tree
Showing 81 changed files with 2,297 additions and 331 deletions.
3 changes: 2 additions & 1 deletion .idea/dictionaries/user.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## [14-5.2] - 2024-12-27
* It is now possible to verify the integrity of file backups as well, partially or fully
* Improve files backup snapshot UI
* Allow changing backup location when USB drive isn't plugged in
* Fix work profile USB backup

## [14-5.1] - 2024-12-10
* First Android 15 release
* New backup format using compression and deduplication
Expand Down
23 changes: 23 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Reporting Security Issues

The Seedvault team and community take security bugs seriously.
We appreciate your efforts to responsibly disclose your findings,
and will make every effort to acknowledge your contributions.

To report a security issue,
please send an email to `security@seedvault.app`
or use the GitHub Security Advisory
["Report a Vulnerability"](https://github.com/seedvault-app/seedvault/security/advisories/new) tab.

The Seedvault team will send a response indicating the next steps in handling your report.
After the initial reply to your report,
we will keep you informed of the progress towards a fix and full announcement,
and may ask for additional information or guidance.

# Older platform branches

Due to API breakage in AOSP versions, we have one branch per major AOSP release,
e.g. `android14` and `android15`.
Note that typically only the latest branch is maintained.
This means that fixes for **security issues do not get backported** to older branches automatically.
Please get in touch if you want to maintain an older branch.
8 changes: 6 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.stevesoltys.seedvault"
android:versionCode="34050010"
android:versionName="14-5.1">
android:versionCode="34050020"
android:versionName="14-5.2">
<!--
The version code is the targeted SDK_VERSION plus 6 digits for our own version code.
The version name is the targeted Android version followed by - and our own version name.
Expand Down Expand Up @@ -136,6 +136,10 @@
android:label="@string/notification_checking_finished_title"
android:launchMode="singleTask"/>

<activity
android:name=".ui.check.FileCheckResultActivity"
android:launchMode="singleTask"/>

<service
android:name=".transport.ConfigurableBackupTransportService"
android:exported="false">
Expand Down
9 changes: 1 addition & 8 deletions app/src/main/java/com/stevesoltys/seedvault/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,7 @@ open class App : Application() {
single { SettingsManager(this@App) }
single { BackupNotificationManager(this@App) }
single { BackendManager(this@App, get(), get(), get()) }
single {
BackendFactory {
// uses context of the device's main user to be able to access USB storage
this@App.applicationContext.getStorageContext {
get<SettingsManager>().getSafProperties()?.isUsb == true
}
}
}
single { BackendFactory() }
single { BackupStateManager(this@App) }
single { Clock() }
factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
Expand Down
16 changes: 10 additions & 6 deletions app/src/main/java/com/stevesoltys/seedvault/BackupStateManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.stevesoltys.seedvault.transport.ConfigurableBackupTransportService
import com.stevesoltys.seedvault.worker.AppBackupPruneWorker
import com.stevesoltys.seedvault.worker.AppBackupWorker.Companion.UNIQUE_WORK_NAME
import com.stevesoltys.seedvault.worker.AppCheckerWorker
import com.stevesoltys.seedvault.worker.FileCheckerWorker
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine

Expand All @@ -36,7 +37,7 @@ class BackupStateManager(
) { appBackupRunning, filesBackupRunning, workInfo1 ->
val workInfoState1 = workInfo1.getOrNull(0)?.state
Log.i(
TAG, "appBackupRunning: $appBackupRunning, " +
TAG, "B - appBackupRunning: $appBackupRunning, " +
"filesBackupRunning: $filesBackupRunning, " +
"appBackupWorker: ${workInfoState1?.name}"
)
Expand All @@ -46,15 +47,18 @@ class BackupStateManager(
val isCheckOrPruneRunning: Flow<Boolean> = combine(
flow = workManager.getWorkInfosForUniqueWorkFlow(AppBackupPruneWorker.UNIQUE_WORK_NAME),
flow2 = workManager.getWorkInfosForUniqueWorkFlow(AppCheckerWorker.UNIQUE_WORK_NAME),
) { pruneInfo, checkInfo ->
flow3 = workManager.getWorkInfosForUniqueWorkFlow(FileCheckerWorker.UNIQUE_WORK_NAME),
) { pruneInfo, appCheckInfo, fileCheckInfo ->
val pruneInfoState = pruneInfo.getOrNull(0)?.state
val checkInfoState = checkInfo.getOrNull(0)?.state
val appCheckState = appCheckInfo.getOrNull(0)?.state
val fileCheckState = fileCheckInfo.getOrNull(0)?.state
Log.i(
TAG,
"pruneBackupWorker: ${pruneInfoState?.name}, " +
"appCheckerWorker: ${checkInfoState?.name}"
"C - pruneBackupWorker: ${pruneInfoState?.name}, " +
"appCheckerWorker: ${appCheckState?.name}, " +
"fileCheckerWorker: ${fileCheckState?.name}"
)
pruneInfoState == RUNNING || checkInfoState == RUNNING
pruneInfoState == RUNNING || appCheckState == RUNNING || fileCheckState == RUNNING
}

val isAutoRestoreEnabled: Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,23 @@ import com.stevesoltys.seedvault.settings.StoragePluginType
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.BackendFactory
import org.calyxos.seedvault.core.backends.BackendProperties
import org.calyxos.seedvault.core.backends.IBackendManager
import org.calyxos.seedvault.core.backends.saf.SafBackend

class BackendManager(
private val context: Context,
private val settingsManager: SettingsManager,
private val blobCache: BlobCache,
backendFactory: BackendFactory,
) {
) : IBackendManager {

@Volatile
private var mBackend: Backend?

@Volatile
private var mBackendProperties: BackendProperties<*>?

val backend: Backend
override val backend: Backend
@Synchronized
get() {
return mBackend ?: error("App plugin was loaded, but still null")
Expand All @@ -42,15 +43,17 @@ class BackendManager(
get() {
return mBackendProperties
}
val isOnRemovableDrive: Boolean get() = backendProperties?.isUsb == true
val requiresNetwork: Boolean get() = backendProperties?.requiresNetwork == true
override val isOnRemovableDrive: Boolean get() = backendProperties?.isUsb == true
override val requiresNetwork: Boolean get() = backendProperties?.requiresNetwork == true

init {
when (settingsManager.storagePluginType) {
StoragePluginType.SAF -> {
val safConfig = settingsManager.getSafProperties() ?: error("No SAF storage saved")
mBackend = backendFactory.createSafBackend(safConfig)
mBackendProperties = safConfig
val safProperties = settingsManager.getSafProperties()
?: error("No SAF storage saved")
val ctx = context.getStorageContext { safProperties.isUsb }
mBackend = backendFactory.createSafBackend(ctx, safProperties)
mBackendProperties = safProperties
}

StoragePluginType.WEB_DAV -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import android.util.Log
import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.getStorageContext
import com.stevesoltys.seedvault.isMassStorage
import com.stevesoltys.seedvault.settings.FlashDrive
import com.stevesoltys.seedvault.settings.SettingsManager
Expand Down Expand Up @@ -58,7 +59,8 @@ internal class SafHandler(
@WorkerThread
@Throws(IOException::class)
suspend fun hasAppBackup(safProperties: SafProperties): Boolean {
val backend = backendFactory.createSafBackend(safProperties)
val context = context.getStorageContext { safProperties.isUsb }
val backend = backendFactory.createSafBackend(context, safProperties)
return backend.getAvailableBackupFileHandles().isNotEmpty()
}

Expand Down Expand Up @@ -92,8 +94,9 @@ internal class SafHandler(

@WorkerThread
fun setPlugin(safProperties: SafProperties) {
val ctx = context.getStorageContext { safProperties.isUsb }
backendManager.changePlugins(
backend = backendFactory.createSafBackend(safProperties),
backend = backendFactory.createSafBackend(ctx, safProperties),
storageProperties = safProperties,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.permitDiskReads
import com.stevesoltys.seedvault.restore.RestoreActivity
import com.stevesoltys.seedvault.settings.BackupPermission.BackupAllowed
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.ui.toRelativeTime
import org.calyxos.seedvault.core.backends.BackendProperties
Expand All @@ -51,6 +52,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
private lateinit var backupScheduling: Preference
private lateinit var backupAppCheck: Preference
private lateinit var backupStorage: TwoStatePreference
private lateinit var backupFileCheck: Preference
private lateinit var backupRecoveryCode: Preference

private val backendProperties: BackendProperties<*>?
Expand Down Expand Up @@ -82,8 +84,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
trySetBackupEnabled(false)
dialog.dismiss()
}
.setNegativeButton(R.string.settings_backup_apk_dialog_cancel) { dialog,
_ -> dialog.dismiss()
.setNegativeButton(R.string.settings_backup_apk_dialog_cancel) { d, _ ->
d.dismiss()
}
.show()
return@OnPreferenceChangeListener false
Expand Down Expand Up @@ -125,6 +127,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
onEnablingStorageBackup()
return@OnPreferenceChangeListener false
}
backupFileCheck = findPreference("backup_file_check")!!

backupRecoveryCode = findPreference("backup_recovery_code")!!
}
Expand All @@ -141,11 +144,16 @@ class SettingsFragment : PreferenceFragmentCompat() {
}
}

viewModel.backupPossible.observe(viewLifecycleOwner) { possible ->
toolbar.menu.findItem(R.id.action_backup)?.isEnabled = possible
toolbar.menu.findItem(R.id.action_restore)?.isEnabled = possible
backupLocation.isEnabled = possible
backupAppCheck.isEnabled = possible
viewModel.backupPossible.observe(viewLifecycleOwner) { permission ->
val allowed = permission == BackupAllowed
toolbar.menu.findItem(R.id.action_backup)?.isEnabled = allowed
toolbar.menu.findItem(R.id.action_restore)?.isEnabled = allowed
// backup location can be changed when backup isn't allowed,
// because flash-drive isn't plugged in
backupLocation.isEnabled = allowed ||
(permission as? BackupPermission.BackupRestricted)?.unavailableUsb == true
backupAppCheck.isEnabled = allowed
backupFileCheck.isEnabled = allowed
}

viewModel.lastBackupTime.observe(viewLifecycleOwner) { time ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.permitDiskReads
import com.stevesoltys.seedvault.repo.Checker
import com.stevesoltys.seedvault.settings.BackupPermission.BackupAllowed
import com.stevesoltys.seedvault.settings.BackupPermission.BackupRestricted
import com.stevesoltys.seedvault.storage.StorageBackupJobService
import com.stevesoltys.seedvault.ui.LiveEvent
import com.stevesoltys.seedvault.ui.MutableLiveEvent
Expand All @@ -47,6 +49,7 @@ import com.stevesoltys.seedvault.worker.AppBackupWorker
import com.stevesoltys.seedvault.worker.AppBackupWorker.Companion.UNIQUE_WORK_NAME
import com.stevesoltys.seedvault.worker.AppCheckerWorker
import com.stevesoltys.seedvault.worker.BackupRequester.Companion.requestFilesAndAppBackup
import com.stevesoltys.seedvault.worker.FileCheckerWorker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
Expand All @@ -63,6 +66,11 @@ import java.util.concurrent.TimeUnit.HOURS
private const val TAG = "SettingsViewModel"
private const val USER_FULL_DATA_BACKUP_AWARE = "user_full_data_backup_aware"

sealed class BackupPermission {
object BackupAllowed : BackupPermission()
class BackupRestricted(val unavailableUsb: Boolean = false) : BackupPermission()
}

internal class SettingsViewModel(
app: Application,
settingsManager: SettingsManager,
Expand All @@ -85,11 +93,13 @@ internal class SettingsViewModel(

private val isBackupRunning: StateFlow<Boolean>
private val isCheckOrPruneRunning: StateFlow<Boolean>
private val mBackupPossible = MutableLiveData(false)
val backupPossible: LiveData<Boolean> = mBackupPossible
private val mBackupPossible = MutableLiveData<BackupPermission>(BackupRestricted())
val backupPossible: LiveData<BackupPermission> = mBackupPossible

private val mBackupSize = MutableLiveData<Long>()
val backupSize: LiveData<Long> = mBackupSize
private val mFilesBackupSize = MutableLiveData<Long>()
val filesBackupSize: LiveData<Long> = mFilesBackupSize

internal val lastBackupTime = settingsManager.lastBackupTime
internal val appBackupWorkInfo =
Expand All @@ -99,9 +109,6 @@ internal class SettingsViewModel(

private val mAppStatusList = lastBackupTime.switchMap {
// updates app list when lastBackupTime changes
// FIXME: Since we are currently updating that time a lot,
// re-fetching everything on each change hammers the system hard
// which can cause android.os.DeadObjectException
getAppStatusResult()
}
internal val appStatusList: LiveData<AppStatusResult> = mAppStatusList
Expand Down Expand Up @@ -183,12 +190,24 @@ internal class SettingsViewModel(
onStoragePropertiesChanged()
}

private fun onBackupRunningStateChanged() {
private suspend fun onBackupRunningStateChanged() = withContext(Dispatchers.IO) {
val backupAllowed = !isBackupRunning.value && !isCheckOrPruneRunning.value
if (backupAllowed) viewModelScope.launch(Dispatchers.IO) {
val canDo = !backendManager.isOnUnavailableUsb()
mBackupPossible.postValue(canDo)
} else mBackupPossible.postValue(false)
if (backupAllowed) {
if (backendManager.isOnUnavailableUsb()) {
updateBackupPossible(BackupRestricted(unavailableUsb = true))
} else {
updateBackupPossible(BackupAllowed)
}
} else updateBackupPossible(BackupRestricted())
}

/**
* Updates [mBackupPossible] on the UiThread to avoid race conditions.
*/
private suspend fun updateBackupPossible(newValue: BackupPermission) {
withContext(Dispatchers.Main) {
mBackupPossible.value = newValue
}
}

private fun onStoragePropertiesChanged() {
Expand Down Expand Up @@ -221,7 +240,7 @@ internal class SettingsViewModel(
networkCallback.registered = true
}
// update whether we can do backups right now or not
onBackupRunningStateChanged()
viewModelScope.launch { onBackupRunningStateChanged() }
}

override fun onCleared() {
Expand Down Expand Up @@ -332,10 +351,20 @@ internal class SettingsViewModel(
}
}

fun loadFileBackupSize() {
viewModelScope.launch(Dispatchers.IO) {
mFilesBackupSize.postValue(storageBackup.getBackupSize())
}
}

fun checkAppBackups(percent: Int) {
AppCheckerWorker.scheduleNow(app, percent)
}

fun checkFileBackups(percent: Int) {
FileCheckerWorker.scheduleNow(app, percent)
}

fun onLogcatUriReceived(uri: Uri?) = viewModelScope.launch(Dispatchers.IO) {
if (uri == null) {
onLogcatError()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ package com.stevesoltys.seedvault.storage
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.crypto.KeyManager
import org.calyxos.backup.storage.api.StorageBackup
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module

val storageModule = module {
single { StorageBackup(get(), { get<BackendManager>().backend }, get<KeyManager>()) }
single { StorageBackup(androidContext(), get<BackendManager>(), get<KeyManager>()) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.settings.SettingsViewModel
import org.koin.androidx.viewmodel.ext.android.activityViewModel

private const val WARN_PERCENT = 25
private const val WARN_BYTES = 1024 * 1024 * 1024 // 1 GB
internal const val WARN_PERCENT = 25
internal const val WARN_BYTES = 1024 * 1024 * 1024 // 1 GB

class AppCheckFragment : Fragment() {

Expand Down
Loading

0 comments on commit 1229cea

Please sign in to comment.