Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ plugins {
}

val versionMajor = 2
val versionMinor = 0
val versionPatch = 2
val versionMinor = 1
val versionPatch = 0

val formattedVersionCode = (versionMajor * 10000 + versionMinor * 100 + versionPatch).toString()
val formattedVersionName = "$versionMajor.$versionMinor.$versionPatch"
Expand Down Expand Up @@ -291,6 +291,7 @@ dependencies {
implementation(libs.androidx.lifecycle.livedata.ktx)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.preference.ktx)
implementation(libs.androidx.work.runtime)

implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.material3)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ public OperationResult<FileDescriptor> process(
Timber.d("Unable to process file, no cached file");

updateProgressStatusForFile(file.getUid(), SyncProgressStatus.IDLE);

return OperationResult.error(
newCacheError(MESSAGE_FAILED_TO_FIND_CACHED_FILE, new Stacktrace()));
}
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/kotlin/com/ivanovsky/passnotes/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.app.Application
import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl
import com.ivanovsky.passnotes.domain.loggingAndReporting.CrashReporterInteractor
import com.ivanovsky.passnotes.domain.loggingAndReporting.LoggerInteractor
import com.ivanovsky.passnotes.domain.worker.BackgroundSyncWorker
import com.ivanovsky.passnotes.injection.AppStartDependencies
import com.ivanovsky.passnotes.injection.DIModuleBuilder
import com.ivanovsky.passnotes.injection.DefaultModuleBuilder
Expand Down Expand Up @@ -55,5 +56,7 @@ open class App : Application() {
androidContext(this@App)
modules(moduleBuilder.buildModules())
}

BackgroundSyncWorker.schedule(settings)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface Settings {
var isPostponedSyncEnabled: Boolean
var autoLockDelayInMs: Int
var autoClearClipboardDelayInMs: Int
var backgroundSyncIntervalInMs: Int
var isLockNotificationVisible: Boolean
var isLockNotificationDialogEnabled: Boolean
var isLockDatabaseOnBack: Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.ivanovsky.passnotes.data.entity.TestToggles
import com.ivanovsky.passnotes.data.repository.keepass.KeepassImplementation
import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.AUTO_CLEAR_CLIPBOARD_DELAY_IN_MS
import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.AUTO_LOCK_DELAY_IN_MS
import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.BACKGROUND_SYNC_INTERVAL_IN_MS
import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.GIT_USER_EMAIL
import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.GIT_USER_NAME
import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.IS_ACTIVATE_SEARCH_ON_START
Expand Down Expand Up @@ -139,6 +140,13 @@ class SettingsImpl(private val context: Context) : Settings {
putString(AUTO_CLEAR_CLIPBOARD_DELAY_IN_MS, value.toString())
}

override var backgroundSyncIntervalInMs: Int
get() = getString(BACKGROUND_SYNC_INTERVAL_IN_MS)?.toInt()
?: (BACKGROUND_SYNC_INTERVAL_IN_MS.defaultValue as String).toInt()
set(value) {
putString(BACKGROUND_SYNC_INTERVAL_IN_MS, value.toString())
}

override var searchOptions: SearchOptions
get() = getString(SEARCH_OPTIONS)?.let {
SearchSettingsConverter.fromString(it)
Expand Down Expand Up @@ -374,6 +382,11 @@ class SettingsImpl(private val context: Context) : Settings {
type = STRING,
defaultValue = TimeUnit.SECONDS.toMillis(30).toString()
),
BACKGROUND_SYNC_INTERVAL_IN_MS(
keyId = R.string.pref_background_sync_interval_in_ms,
type = STRING,
defaultValue = TimeUnit.DAYS.toMillis(1).toString()
),

// String prefs
SEARCH_OPTIONS(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,88 @@
package com.ivanovsky.passnotes.domain.usecases

import arrow.core.Either
import arrow.core.raise.either
import com.ivanovsky.passnotes.data.entity.ConflictResolutionStrategy
import com.ivanovsky.passnotes.data.entity.FileDescriptor
import com.ivanovsky.passnotes.data.entity.OperationError
import com.ivanovsky.passnotes.data.entity.OperationError.newGenericError
import com.ivanovsky.passnotes.data.entity.OperationResult
import com.ivanovsky.passnotes.data.entity.SyncConflictInfo
import com.ivanovsky.passnotes.data.entity.SyncProgressStatus
import com.ivanovsky.passnotes.data.entity.SyncState
import com.ivanovsky.passnotes.data.entity.SyncStatus
import com.ivanovsky.passnotes.data.repository.EncryptedDatabaseRepository
import com.ivanovsky.passnotes.data.repository.UsedFileRepository
import com.ivanovsky.passnotes.data.repository.file.FSOptions
import com.ivanovsky.passnotes.data.repository.file.FileSystemResolver
import com.ivanovsky.passnotes.data.repository.file.OnConflictStrategy
import com.ivanovsky.passnotes.data.repository.file.SyncStrategy
import com.ivanovsky.passnotes.domain.DispatcherProvider
import com.ivanovsky.passnotes.extensions.getFileDescriptor
import com.ivanovsky.passnotes.extensions.isSameFile
import com.ivanovsky.passnotes.extensions.isSyncable
import com.ivanovsky.passnotes.extensions.toEither
import kotlinx.coroutines.withContext
import timber.log.Timber

class SyncUseCases(
private val fileSystemResolver: FileSystemResolver,
private val dispatchers: DispatcherProvider,
private val dbRepo: EncryptedDatabaseRepository
private val dbRepo: EncryptedDatabaseRepository,
private val usedFileRepository: UsedFileRepository
) {

suspend fun syncChanges(): Either<OperationError, Unit> =
withContext(dispatchers.IO) {
either {
val syncableFiles = usedFileRepository.getAll()
.map { file -> file.getFileDescriptor() }
.filter { file -> file.fsAuthority.isSyncable() }

for (file in syncableFiles) {
val provider = fileSystemResolver.resolveProvider(file.fsAuthority)
val cachedFile = provider.syncProcessor.getCachedFile(file.uid)

if (cachedFile == null) {
// The file isn't downloaded, it should be downloaded first
Timber.d(
"Syncing file: file=%s, fsType=%s".format(
file.path,
file.fsAuthority.type
)
)

val content = provider.openFileForRead(
file,
OnConflictStrategy.CANCEL,
FSOptions.READ_ONLY
).toEither().bind()

Either.catch { content.close() }
.mapLeft { error -> newGenericError(error) }
.bind()
} else {
val syncState = getSyncState(file)

val hasRemoteChanges = (syncState.status == SyncStatus.REMOTE_CHANGES)
val hasLocalChanges = (syncState.status == SyncStatus.LOCAL_CHANGES)

Timber.d(
"Syncing file: syncState=%s, file=%s, fsType=%s".format(
syncState,
file.path,
file.fsAuthority.type
)
)

if (hasRemoteChanges || hasLocalChanges) {
processSync(file).toEither().bind()
}
}
}
}
}

suspend fun getSyncConflictInfo(file: FileDescriptor): OperationResult<SyncConflictInfo> =
withContext(dispatchers.IO) {
fileSystemResolver
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.ivanovsky.passnotes.domain.worker

import android.content.Context
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.ivanovsky.passnotes.data.repository.settings.Settings
import com.ivanovsky.passnotes.domain.usecases.SyncUseCases
import com.ivanovsky.passnotes.injection.GlobalInjector
import com.ivanovsky.passnotes.injection.GlobalInjector.inject
import java.util.concurrent.TimeUnit
import timber.log.Timber

class BackgroundSyncWorker(
appContext: Context,
workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {

private val syncUseCases: SyncUseCases by inject()

override suspend fun doWork(): Result {
Timber.d("Periodic sync worker started")

return syncUseCases.syncChanges().fold(
ifLeft = { error ->
Timber.e("Sync failed: %s".format(error))
Timber.e(error.throwable)

Result.retry()
},
ifRight = {
Timber.d("Sync finished successfully")

Result.success()
}
)
}

companion object {
private const val BACKGROUND_SYNC_WORKER_NAME = "background-sync-worker"

fun schedule(settings: Settings) {
val context: Context = GlobalInjector.get()
val interval = settings.backgroundSyncIntervalInMs

Timber.d("Schedule background worker: interval=%s".format(interval))

if (interval == -1) {
WorkManager.getInstance(context).cancelUniqueWork(BACKGROUND_SYNC_WORKER_NAME)
return
}

val constraints = Constraints.Builder()
.setRequiredNetworkRequest(
NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.build(),
NetworkType.CONNECTED
)
.build()

val request = PeriodicWorkRequestBuilder<BackgroundSyncWorker>(
interval.toLong(),
TimeUnit.MILLISECONDS
)
.setConstraints(constraints)
.build()

WorkManager.getInstance(context).enqueueUniquePeriodicWork(
BACKGROUND_SYNC_WORKER_NAME,
ExistingPeriodicWorkPolicy.UPDATE,
request
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.ivanovsky.passnotes.extensions

import com.ivanovsky.passnotes.data.entity.FSAuthority

fun FSAuthority.isSyncable(): Boolean {
return credentials != null
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ fun FSType.getLoginType(): LoginType? {
FSType.FAKE -> LoginType.USERNAME_PASSWORD
else -> null
}
}
}

// fun FSType.is
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ object UseCaseModule {
single { GetTestPasswordUseCase(get(), get()) }
single { LockDatabaseUseCase() }
single { GetRecentlyOpenedFilesUseCase(get(), get()) }
single { SyncUseCases(get(), get(), get()) }
single { SyncUseCases(get(), get(), get(), get()) }
single { AddTemplatesUseCase(get(), get(), get()) }
single { GetDatabaseUseCase(get()) }
single { MoveNoteUseCase(get(), get(), get()) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,15 @@ private fun SelectorItem(
text = item.title,
style = PrimaryTextStyle()
)
Text(
text = item.description,
style = SecondaryTextStyle(
fontSize = AppTheme.theme.textMetrics.secondary
),
modifier = Modifier.padding(top = QuarterMargin)
)
item.description?.let { description ->
Text(
text = description,
style = SecondaryTextStyle(
fontSize = AppTheme.theme.textMetrics.secondary
),
modifier = Modifier.padding(top = QuarterMargin)
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class SelectorDialogItem(
val title: String,
val description: String
val description: String? = null
) : Parcelable
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.ivanovsky.passnotes.R
import com.ivanovsky.passnotes.data.repository.settings.Settings
import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.AUTO_CLEAR_CLIPBOARD_DELAY_IN_MS
import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.AUTO_LOCK_DELAY_IN_MS
import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.BACKGROUND_SYNC_INTERVAL_IN_MS
import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.IS_ACTIVATE_SEARCH_ON_START
import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.IS_BIOMETRIC_UNLOCK_ENABLED
import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.IS_CRASH_REPORTING_ENABLED
Expand Down Expand Up @@ -47,6 +48,7 @@ class AppSettingsFragment : BasePreferenceFragment(), PermissionRequestResultRec

private lateinit var isFileLogEnabledPref: SwitchPreferenceCompat
private lateinit var isCrashReportingEnabledPref: SwitchPreferenceCompat
private lateinit var backgroundSyncIntervalPref: Preference
private lateinit var isPostponedSyncEnabledPref: SwitchPreferenceCompat
private lateinit var isBiometricUnlockEnabledPref: SwitchPreferenceCompat
private lateinit var keepassImplementationPref: Preference
Expand Down Expand Up @@ -88,6 +90,7 @@ class AppSettingsFragment : BasePreferenceFragment(), PermissionRequestResultRec
IS_LOCK_DATABASE_ON_BACK,
AUTO_LOCK_DELAY_IN_MS,
AUTO_CLEAR_CLIPBOARD_DELAY_IN_MS,
BACKGROUND_SYNC_INTERVAL_IN_MS,
IS_FILE_LOG_ENABLED,
IS_CRASH_REPORTING_ENABLED,
IS_POSTPONED_SYNC_ENABLED,
Expand All @@ -113,6 +116,11 @@ class AppSettingsFragment : BasePreferenceFragment(), PermissionRequestResultRec
)
?: throwPreferenceNotFound(R.string.pref_is_crash_reporting_enabled)

backgroundSyncIntervalPref = findPreference(
getString(R.string.pref_background_sync_interval_in_ms)
)
?: throwPreferenceNotFound(R.string.pref_background_sync_interval_in_ms)

isPostponedSyncEnabledPref = findPreference(
getString(R.string.pref_is_postponed_sync_enabled)
)
Expand Down Expand Up @@ -150,6 +158,11 @@ class AppSettingsFragment : BasePreferenceFragment(), PermissionRequestResultRec
categoryLogging = findPreference(getString(R.string.pref_category_reporting))
?: throwPreferenceNotFound(R.string.pref_category_reporting)

backgroundSyncIntervalPref.setOnPreferenceChangeListener { _, newValue ->
viewModel.onBackgroundSyncIntervalSelected((newValue as String).toInt())
true
}

isFileLogEnabledPref.setOnPreferenceChangeListener { _, newValue ->
viewModel.onFileLogEnabledChanged(newValue as Boolean)
true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.ivanovsky.passnotes.domain.biometric.BiometricResolver
import com.ivanovsky.passnotes.domain.entity.SystemPermission
import com.ivanovsky.passnotes.domain.interactor.settings.app.AppSettingsInteractor
import com.ivanovsky.passnotes.domain.loggingAndReporting.CrashReporterInteractor.CrashReporterAvailability
import com.ivanovsky.passnotes.domain.worker.BackgroundSyncWorker
import com.ivanovsky.passnotes.extensions.formatReadableMessage
import com.ivanovsky.passnotes.presentation.core.dialog.selectorDialog.SelectorDialogArgs
import com.ivanovsky.passnotes.presentation.core.dialog.selectorDialog.model.SelectorDialogItem
Expand Down Expand Up @@ -142,6 +143,16 @@ class AppSettingsViewModel(
interactor.lockDatabase()
}

fun onBackgroundSyncIntervalSelected(intervalInMs: Int) {
val isChanged = (settings.backgroundSyncIntervalInMs != intervalInMs)

settings.backgroundSyncIntervalInMs = intervalInMs

if (isChanged) {
BackgroundSyncWorker.schedule(settings)
}
}

fun onSendLongFileClicked() {
isLoading.value = true

Expand Down
Loading
Loading