Added EventManager Domain/Data and Event Listeners.

Added EventManager LogTags.
Added EventManagerConfig and EventManagerConfigProvider.
Added EventWorkManager and EventManagerWorker.

Added UserEventListener.
Added UserAddressEventListener.
Added MailSettingsEventListener.
Added UserSettingsEventListener.
Added ContactEventListener.
Added ContactEmailsEventListener.

Added AppLifecycleObserver and AppLifecycleProvider.
This commit is contained in:
Neil Marietta 2021-06-23 16:55:23 +02:00
parent 14d4416683
commit 351fa5eb7b
94 changed files with 5901 additions and 31 deletions

View File

@ -1,3 +1,147 @@
## Version [1.18]
25 Oct, 2021
### Dependencies
- Contact 1.18.
- Domain 1.18.
- EventManager 1.18.
- MailSettings 1.18.
- Presentation 1.18.
- User 1.18.
- UserSettings 1.18.
### New Migration
- Please apply changes as follow to your AppDatabase:
- Add ```EventMetadataEntity``` to your AppDatabase ```entities```.
- Add ```EventManagerConverters``` to your ```TypeConverters```.
- Extends ```EventMetadataDatabase```.
- Add a migration to your AppDatabase (```addMigration```):
```
val MIGRATION_X_Y = object : Migration(X, Y) {
override fun migrate(database: SupportSQLiteDatabase) {
EventMetadataDatabase.MIGRATION_0.migrate(database)
}
}
```
### New Dagger Module
- To provide the various EventManager components:
```
@Module
@InstallIn(SingletonComponent::class)
object EventManagerModule {
@Provides
@Singleton
@EventManagerCoroutineScope
fun provideEventManagerCoroutineScope(): CoroutineScope =
CoroutineScope(Dispatchers.Default + SupervisorJob())
@Provides
@Singleton
@JvmSuppressWildcards
fun provideEventManagerProvider(
eventManagerFactory: EventManagerFactory,
eventListeners: Set<EventListener<*, *>>
): EventManagerProvider =
EventManagerProviderImpl(eventManagerFactory, eventListeners)
@Provides
@Singleton
fun provideEventMetadataRepository(
db: EventMetadataDatabase,
provider: ApiProvider
): EventMetadataRepository = EventMetadataRepositoryImpl(db, provider)
@Provides
@Singleton
fun provideEventWorkManager(
workManager: WorkManager,
appLifecycleProvider: AppLifecycleProvider
): EventWorkManager = EventWorkManagerImpl(workManager, appLifecycleProvider)
@Provides
@Singleton
@ElementsIntoSet
@JvmSuppressWildcards
fun provideEventListenerSet(
userEventListener: UserEventListener,
userAddressEventListener: UserAddressEventListener,
userSettingsEventListener: UserSettingsEventListener,
mailSettingsEventListener: MailSettingsEventListener,
contactEventListener: ContactEventListener,
contactEmailEventListener: ContactEmailEventListener,
): Set<EventListener<*, *>> = setOf(
userEventListener,
userAddressEventListener,
userSettingsEventListener,
mailSettingsEventListener,
contactEventListener,
contactEmailEventListener,
)
}
```
- To provide AppLifecycleObserver, AppLifecycleProvider and WorkManager (needed by EventManager):
```
@Module
@InstallIn(SingletonComponent::class)
object ApplicationModule {
...
@Provides
@Singleton
fun provideAppLifecycleObserver(): AppLifecycleObserver =
AppLifecycleObserver()
@Provides
@Singleton
fun provideWorkManager(@ApplicationContext context: Context): WorkManager =
WorkManager.getInstance(context)
...
}
@Module
@InstallIn(SingletonComponent::class)
abstract class ApplicationBindsModule {
@Binds
abstract fun provideAppLifecycleStateProvider(observer: AppLifecycleObserver): AppLifecycleProvider
}
```
### WorkManager
**Initialization of WorkManager is up to the client.**
EventManager/EventWorker assume the client support injecting:
```
@HiltWorker
open class EventWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
...
```
Please refer to:
- https://developer.android.com/training/dependency-injection/hilt-jetpack#workmanager
- https://developer.android.com/topic/libraries/architecture/workmanager/advanced/custom-configuration.
### Changes
- Added EventManager Domain/Data and EventListeners.
- Added EventManager LogTags.
- Added EventManagerConfig and EventManagerConfigProvider.
- Added EventWorkManager and EventManagerWorker.
- Added UserEventListener.
- Added UserAddressEventListener.
- Added MailSettingsEventListener.
- Added UserSettingsEventListener.
- Added ContactEventListener.
- Added ContactEmailsEventListener.
- Added AppLifecycleObserver and AppLifecycleProvider.
## Presentation [1.17.0]
### Changes

View File

@ -154,6 +154,14 @@ Account Manager Data Db: **1.16** - _released on: Oct 19, 2021_
Account Manager Dagger: **1.16** - _released on: Oct 19, 2021_
### Event Manager
Event Manager: **0** - _released on: Jun 17, 2021_
Event Manager Domain: **0** - _released on: Jun 17, 2021_
Event Manager Data: **0** - _released on: Jun 17, 2021_
### Key
Key: **1.15.6** - _released on: Oct 15, 2021_

View File

@ -23,7 +23,7 @@ plugins {
kotlin("android")
}
libVersion = Version(1, 16, channel = Version.Channel.Build, build = 6)
libVersion = Version(1, 18, 0)
android()

View File

@ -39,6 +39,7 @@ dependencies {
project(Module.domain),
project(Module.contactDomain),
project(Module.userData),
project(Module.eventManagerDomain),
// Kotlin
`serialization-json`,

View File

@ -0,0 +1,102 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.contact.data
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import me.proton.core.contact.data.api.resource.ContactEmailResource
import me.proton.core.contact.data.local.db.ContactDatabase
import me.proton.core.contact.domain.entity.ContactEmailId
import me.proton.core.contact.domain.entity.ContactId
import me.proton.core.contact.domain.repository.ContactLocalDataSource
import me.proton.core.contact.domain.repository.ContactRepository
import me.proton.core.domain.entity.UserId
import me.proton.core.eventmanager.domain.EventListener
import me.proton.core.eventmanager.domain.entity.Action
import me.proton.core.eventmanager.domain.entity.Event
import me.proton.core.eventmanager.domain.entity.EventsResponse
import me.proton.core.util.kotlin.deserializeOrNull
import javax.inject.Inject
import javax.inject.Singleton
@Serializable
data class ContactEmailsEvents(
@SerialName("ContactEmails")
val contactEmails: List<ContactEmailEvent>
)
@Serializable
data class ContactEmailEvent(
@SerialName("ID")
val id: String,
@SerialName("Action")
val action: Int,
@SerialName("ContactEmail")
val contactEmail: ContactEmailResource? = null
)
@Singleton
class ContactEmailEventListener @Inject constructor(
private val db: ContactDatabase,
private val contactLocalDataSource: ContactLocalDataSource,
private val contactRepository: ContactRepository,
private val contactEventListener: ContactEventListener
) : EventListener<String, ContactEmailResource>() {
override val type = Type.Core
override val order = 2
override suspend fun deserializeEvents(response: EventsResponse): List<Event<String, ContactEmailResource>>? {
return response.body.deserializeOrNull<ContactEmailsEvents>()?.contactEmails?.map {
Event(requireNotNull(Action.map[it.action]), it.id, it.contactEmail)
}
}
override suspend fun <R> inTransaction(block: suspend () -> R): R {
return db.inTransaction(block)
}
override suspend fun onPrepare(userId: UserId, entities: List<ContactEmailResource>) {
// Don't fetch Contacts that will be created in this set of modifications.
val contactActions = contactEventListener.getActionMap(userId)
val createContactIds = contactActions[Action.Create].orEmpty().map { it.key }.toHashSet()
// Make sure we'll fetch other Contacts.
entities.filterNot { createContactIds.contains(it.contactId) }.forEach {
contactRepository.getContactWithCards(userId, ContactId(it.contactId), refresh = false)
}
}
override suspend fun onCreate(userId: UserId, entities: List<ContactEmailResource>) {
entities.forEach { contactLocalDataSource.upsertContactEmails(it.toContactEmail(userId)) }
}
override suspend fun onUpdate(userId: UserId, entities: List<ContactEmailResource>) {
entities.forEach { contactLocalDataSource.upsertContactEmails(it.toContactEmail(userId)) }
}
override suspend fun onDelete(userId: UserId, keys: List<String>) {
contactLocalDataSource.deleteContactEmails(*keys.map { ContactEmailId(it) }.toTypedArray())
}
override suspend fun onResetAll(userId: UserId) {
// Already handled in ContactEventListener:
// contactLocalDataSource.deleteAllContactEmails(userId)
// contactRepository.getAllContactEmails(userId, refresh = true)
}
}

View File

@ -0,0 +1,89 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.contact.data
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import me.proton.core.contact.data.api.resource.ContactWithCardsResource
import me.proton.core.contact.data.local.db.ContactDatabase
import me.proton.core.contact.domain.entity.ContactId
import me.proton.core.contact.domain.repository.ContactLocalDataSource
import me.proton.core.contact.domain.repository.ContactRepository
import me.proton.core.domain.entity.UserId
import me.proton.core.eventmanager.domain.EventListener
import me.proton.core.eventmanager.domain.entity.Action
import me.proton.core.eventmanager.domain.entity.Event
import me.proton.core.eventmanager.domain.entity.EventsResponse
import me.proton.core.util.kotlin.deserializeOrNull
import javax.inject.Inject
import javax.inject.Singleton
@Serializable
data class ContactsEvents(
@SerialName("Contacts")
val contacts: List<ContactEvent>
)
@Serializable
data class ContactEvent(
@SerialName("ID")
val id: String,
@SerialName("Action")
val action: Int,
@SerialName("Contact")
val contact: ContactWithCardsResource? = null
)
@Singleton
class ContactEventListener @Inject constructor(
private val db: ContactDatabase,
private val contactLocalDataSource: ContactLocalDataSource,
private val contactRepository: ContactRepository
) : EventListener<String, ContactWithCardsResource>() {
override val type = Type.Core
override val order = 1
override suspend fun deserializeEvents(response: EventsResponse): List<Event<String, ContactWithCardsResource>>? {
return response.body.deserializeOrNull<ContactsEvents>()?.contacts?.map {
Event(requireNotNull(Action.map[it.action]), it.id, it.contact)
}
}
override suspend fun <R> inTransaction(block: suspend () -> R): R {
return db.inTransaction(block)
}
override suspend fun onCreate(userId: UserId, entities: List<ContactWithCardsResource>) {
entities.forEach { contactLocalDataSource.upsertContactWithCards(it.toContactWithCards(userId)) }
}
override suspend fun onUpdate(userId: UserId, entities: List<ContactWithCardsResource>) {
entities.forEach { contactLocalDataSource.upsertContactWithCards(it.toContactWithCards(userId)) }
}
override suspend fun onDelete(userId: UserId, keys: List<String>) {
contactLocalDataSource.deleteContacts(*keys.map { ContactId(it) }.toTypedArray())
}
override suspend fun onResetAll(userId: UserId) {
contactLocalDataSource.deleteAllContacts(userId)
contactRepository.getAllContacts(userId, refresh = true)
}
}

View File

@ -95,6 +95,10 @@ class ContactLocalDataSourceImpl @Inject constructor(
contactDatabase.contactDao().deleteAllContacts(userId)
}
override suspend fun deleteAllContactEmails(userId: UserId) {
contactDatabase.contactEmailDao().deleteAllContactsEmails(userId)
}
override suspend fun deleteAllContacts() {
contactDatabase.contactDao().deleteAllContacts()
}

View File

@ -30,7 +30,7 @@ import me.proton.core.data.room.db.BaseDao
import me.proton.core.domain.entity.UserId
@Dao
abstract class ContactDao: BaseDao<ContactEntity>() {
abstract class ContactDao : BaseDao<ContactEntity>() {
@Transaction
@Query("SELECT * FROM ContactEntity WHERE contactId = :contactId")
abstract fun observeContact(contactId: ContactId): Flow<ContactWithMailsAndCardsRelation?>

View File

@ -45,7 +45,7 @@ import javax.inject.Singleton
@Singleton
class ContactRepositoryImpl @Inject constructor(
private val remoteDataSource: ContactRemoteDataSource,
private val localDataSource: ContactLocalDataSource
private val localDataSource: ContactLocalDataSource,
) : ContactRepository {
private data class ContactStoreKey(val userId: UserId, val contactId: ContactId)

View File

@ -33,11 +33,13 @@ dependencies {
project(Module.cryptoCommon),
project(Module.userDomain),
project(Module.keyDomain),
project(Module.eventManagerDomain),
// Kotlin
`kotlin-jdk8`,
`coroutines-core`,
`ez-vcard`
`ez-vcard`,
`javax-inject`
)
testImplementation(project(Module.kotlinTest))

View File

@ -74,6 +74,11 @@ interface ContactLocalDataSource {
*/
suspend fun deleteAllContacts(userId: UserId)
/**
* Delete all contact emails for [userId].
*/
suspend fun deleteAllContactEmails(userId: UserId)
/**
* Delete all contacts, for every user.
*/
@ -89,4 +94,4 @@ interface ContactLocalDataSource {
* @throws SQLiteConstraintException if corresponding user(s) doesn't exist.
*/
suspend fun mergeContacts(vararg contacts: Contact)
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.contact.domain.usecase
import me.proton.core.contact.domain.entity.ContactCard
import me.proton.core.contact.domain.entity.ContactId
import me.proton.core.contact.domain.repository.ContactRepository
import me.proton.core.domain.entity.UserId
import me.proton.core.eventmanager.domain.EventManagerConfig
import me.proton.core.eventmanager.domain.EventManagerProvider
import me.proton.core.eventmanager.domain.extension.suspend
import javax.inject.Inject
class UpdateContactRemote @Inject constructor(
private val contactRepository: ContactRepository,
private val eventManagerProvider: EventManagerProvider,
) {
// Called by UpdateContactWorker (unique name: userId+contactId, ExistingWorkPolicy.APPEND_OR_REPLACE).
// Prerequisite: Local optimistic update Contact.
suspend operator fun invoke(userId: UserId, contactId: ContactId, contactCards: List<ContactCard>) {
eventManagerProvider.suspend(EventManagerConfig.Core(userId)) {
contactRepository.updateContact(userId, contactId, contactCards)
}
}
}

View File

@ -80,6 +80,7 @@ dependencies {
project(Module.contactHilt),
project(Module.crypto),
project(Module.domain),
project(Module.eventManager),
project(Module.gopenpgp),
project(Module.humanVerification),
project(Module.key),
@ -97,14 +98,15 @@ dependencies {
// Android
`activity`,
`appcompat`,
`android-work-runtime`,
`constraint-layout`,
`fragment`,
`gotev-cookieStore`,
`hilt-android`,
`hilt-androidx-workManager`,
`lifecycle-extensions`,
`lifecycle-viewModel`,
`hilt-androidx-annotations`,
`material`,
`android-work-runtime`,
// Other
`room-ktx`,
@ -116,7 +118,8 @@ dependencies {
kapt(
`hilt-android-compiler`,
`hilt-androidx-compiler`,
`room-compiler`
`room-compiler`,
`lifecycle-compiler`
)
// Test

File diff suppressed because it is too large Load Diff

View File

@ -31,6 +31,16 @@
android:supportsRtl="true"
android:theme="@style/ProtonTheme"
tools:replace="android:theme">
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
<activity
android:name=".MainActivity"
android:theme="@style/ProtonTheme.Splash">

View File

@ -21,13 +21,23 @@ package me.proton.android.core.coreexample
import android.app.Application
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate.setCompatVectorFromResourcesEnabled
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import dagger.hilt.android.HiltAndroidApp
import me.proton.core.eventmanager.data.CoreEventManagerStarter
import me.proton.core.util.kotlin.CoreLogger
import timber.log.Timber
import timber.log.Timber.DebugTree
import javax.inject.Inject
@HiltAndroidApp
class CoreExampleApp : Application() {
class CoreExampleApp : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory
@Inject
lateinit var coreEventManagerStarter: CoreEventManagerStarter
private class CrashReportingTree : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, e: Throwable?) {
@ -54,5 +64,8 @@ class CoreExampleApp : Application() {
}
setCompatVectorFromResourcesEnabled(true)
coreEventManagerStarter.start()
}
override fun getWorkManagerConfiguration() = Configuration.Builder().setWorkerFactory(workerFactory).build()
}

View File

@ -36,6 +36,9 @@ import me.proton.core.contact.data.local.db.entity.ContactEntity
import me.proton.core.crypto.android.keystore.CryptoConverters
import me.proton.core.data.room.db.BaseDatabase
import me.proton.core.data.room.db.CommonConverters
import me.proton.core.eventmanager.data.db.EventManagerConverters
import me.proton.core.eventmanager.data.db.EventMetadataDatabase
import me.proton.core.eventmanager.data.entity.EventMetadataEntity
import me.proton.core.humanverification.data.db.HumanVerificationConverters
import me.proton.core.humanverification.data.db.HumanVerificationDatabase
import me.proton.core.humanverification.data.entity.HumanVerificationEntity
@ -90,6 +93,8 @@ import me.proton.core.usersettings.data.entity.UserSettingsEntity
ContactCardEntity::class,
ContactEmailEntity::class,
ContactEmailLabelEntity::class,
// event-manager
EventMetadataEntity::class,
],
version = AppDatabase.version,
exportSchema = true
@ -101,7 +106,8 @@ import me.proton.core.usersettings.data.entity.UserSettingsEntity
CryptoConverters::class,
HumanVerificationConverters::class,
UserSettingsConverters::class,
ContactConverters::class
ContactConverters::class,
EventManagerConverters::class,
)
abstract class AppDatabase :
BaseDatabase(),
@ -114,11 +120,12 @@ abstract class AppDatabase :
MailSettingsDatabase,
UserSettingsDatabase,
OrganizationDatabase,
ContactDatabase {
ContactDatabase,
EventMetadataDatabase {
companion object {
const val name = "db-account-manager"
const val version = 10
const val version = 11
val migrations = listOf(
AppDatabaseMigrations.MIGRATION_1_2,
@ -130,6 +137,7 @@ abstract class AppDatabase :
AppDatabaseMigrations.MIGRATION_7_8,
AppDatabaseMigrations.MIGRATION_8_9,
AppDatabaseMigrations.MIGRATION_9_10,
AppDatabaseMigrations.MIGRATION_10_11,
)
fun buildDatabase(context: Context): AppDatabase =

View File

@ -22,6 +22,7 @@ import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import me.proton.core.account.data.db.AccountDatabase
import me.proton.core.contact.data.local.db.ContactDatabase
import me.proton.core.eventmanager.data.db.EventMetadataDatabase
import me.proton.core.humanverification.data.db.HumanVerificationDatabase
import me.proton.core.key.data.db.KeySaltDatabase
import me.proton.core.key.data.db.PublicAddressDatabase
@ -93,4 +94,10 @@ object AppDatabaseMigrations {
PublicAddressDatabase.MIGRATION_1.migrate(database)
}
}
val MIGRATION_10_11 = object : Migration(10, 11) {
override fun migrate(database: SupportSQLiteDatabase) {
EventMetadataDatabase.MIGRATION_0.migrate(database)
}
}
}

View File

@ -28,6 +28,7 @@ import dagger.hilt.components.SingletonComponent
import me.proton.android.core.coreexample.db.AppDatabase
import me.proton.core.account.data.db.AccountDatabase
import me.proton.core.contact.data.local.db.ContactDatabase
import me.proton.core.eventmanager.data.db.EventMetadataDatabase
import me.proton.core.humanverification.data.db.HumanVerificationDatabase
import me.proton.core.key.data.db.KeySaltDatabase
import me.proton.core.key.data.db.PublicAddressDatabase
@ -79,4 +80,7 @@ abstract class AppDatabaseBindsModule {
@Binds
abstract fun provideContactDatabase(appDatabase: AppDatabase): ContactDatabase
@Binds
abstract fun provideEventMetadataDatabase(appDatabase: AppDatabase): EventMetadataDatabase
}

View File

@ -18,15 +18,21 @@
package me.proton.android.core.coreexample.di
import android.content.Context
import androidx.work.WorkManager
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import me.proton.android.core.coreexample.api.CoreExampleRepository
import me.proton.core.account.domain.entity.AccountType
import me.proton.core.auth.domain.ClientSecret
import me.proton.core.domain.entity.Product
import me.proton.core.network.data.ApiProvider
import me.proton.core.presentation.app.AppLifecycleObserver
import me.proton.core.presentation.app.AppLifecycleProvider
import javax.inject.Singleton
@Module
@ -51,4 +57,21 @@ object ApplicationModule {
@Singleton
fun provideCoreExampleRepository(apiProvider: ApiProvider): CoreExampleRepository =
CoreExampleRepository(apiProvider)
@Provides
@Singleton
fun provideAppLifecycleObserver(): AppLifecycleObserver =
AppLifecycleObserver()
@Provides
@Singleton
fun provideWorkManager(@ApplicationContext context: Context): WorkManager =
WorkManager.getInstance(context)
}
@Module
@InstallIn(SingletonComponent::class)
abstract class ApplicationBindsModule {
@Binds
abstract fun provideAppLifecycleStateProvider(observer: AppLifecycleObserver): AppLifecycleProvider
}

View File

@ -0,0 +1,102 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.android.core.coreexample.di
import androidx.work.WorkManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ElementsIntoSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import me.proton.core.contact.data.ContactEmailEventListener
import me.proton.core.contact.data.ContactEventListener
import me.proton.core.eventmanager.data.EventManagerCoroutineScope
import me.proton.core.eventmanager.data.EventManagerFactory
import me.proton.core.eventmanager.data.EventManagerProviderImpl
import me.proton.core.eventmanager.data.db.EventMetadataDatabase
import me.proton.core.eventmanager.data.repository.EventMetadataRepositoryImpl
import me.proton.core.eventmanager.data.work.EventWorkerManagerImpl
import me.proton.core.eventmanager.domain.EventListener
import me.proton.core.eventmanager.domain.EventManagerProvider
import me.proton.core.eventmanager.domain.repository.EventMetadataRepository
import me.proton.core.eventmanager.domain.work.EventWorkerManager
import me.proton.core.mailsettings.data.MailSettingsEventListener
import me.proton.core.network.data.ApiProvider
import me.proton.core.presentation.app.AppLifecycleProvider
import me.proton.core.user.data.UserAddressEventListener
import me.proton.core.user.data.UserEventListener
import me.proton.core.usersettings.data.UserSettingsEventListener
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object EventManagerModule {
@Provides
@Singleton
@EventManagerCoroutineScope
fun provideEventManagerCoroutineScope(): CoroutineScope =
CoroutineScope(Dispatchers.Default + SupervisorJob())
@Provides
@Singleton
@JvmSuppressWildcards
fun provideEventManagerProvider(
eventManagerFactory: EventManagerFactory,
eventListeners: Set<EventListener<*, *>>
): EventManagerProvider =
EventManagerProviderImpl(eventManagerFactory, eventListeners)
@Provides
@Singleton
fun provideEventMetadataRepository(
db: EventMetadataDatabase,
provider: ApiProvider
): EventMetadataRepository = EventMetadataRepositoryImpl(db, provider)
@Provides
@Singleton
fun provideEventWorkManager(
workManager: WorkManager,
appLifecycleProvider: AppLifecycleProvider
): EventWorkerManager = EventWorkerManagerImpl(workManager, appLifecycleProvider)
@Provides
@Singleton
@ElementsIntoSet
@JvmSuppressWildcards
fun provideEventListenerSet(
userEventListener: UserEventListener,
userAddressEventListener: UserAddressEventListener,
userSettingsEventListener: UserSettingsEventListener,
mailSettingsEventListener: MailSettingsEventListener,
contactEventListener: ContactEventListener,
contactEmailEventListener: ContactEmailEventListener,
): Set<EventListener<*, *>> = setOf(
userEventListener,
userAddressEventListener,
userSettingsEventListener,
mailSettingsEventListener,
contactEventListener,
contactEmailEventListener,
)
}

View File

@ -25,7 +25,6 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import me.proton.android.core.coreexample.Constants
import me.proton.core.crypto.common.context.CryptoContext
import me.proton.core.crypto.common.keystore.KeyStoreCrypto
import me.proton.core.key.data.db.KeySaltDatabase
import me.proton.core.key.data.db.PublicAddressDatabase
import me.proton.core.key.data.repository.KeySaltRepositoryImpl

View File

@ -59,7 +59,7 @@ class AccountViewModel @Inject constructor(
private val userManager: UserManager,
private val humanVerificationManager: HumanVerificationManager,
private var authOrchestrator: AuthOrchestrator,
private var humanVerificationOrchestrator: HumanVerificationOrchestrator
private var humanVerificationOrchestrator: HumanVerificationOrchestrator,
) : ViewModel() {
private val _state = MutableStateFlow(State.Processing as State)

View File

@ -51,6 +51,7 @@ import me.proton.core.user.domain.UserManager
import me.proton.core.user.domain.entity.User
import me.proton.core.util.kotlin.CoreLogger
import me.proton.core.util.kotlin.exhaustive
import me.proton.core.util.kotlin.truncateToLength
import javax.inject.Inject
@HiltViewModel
@ -109,7 +110,7 @@ class ContactDetailViewModel @Inject constructor(
contact.contactCards.map { decryptContactCard(it) }
}
return ViewState.Success(
rawContact = contact.prettyPrint(),
rawContact = contact.prettyPrint().truncateToLength(10000).toString(),
vCardContact = decryptedCards.prettyPrint()
)
}

View File

@ -21,6 +21,7 @@ package me.proton.android.core.coreexample.viewmodel
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.transformLatest
@ -62,6 +63,7 @@ class PublicAddressViewModel @Inject constructor(
fun getPublicAddressState() = accountManager.getPrimaryAccount()
.flatMapLatest { primary -> primary?.let { userManager.getUserFlow(it.userId) } ?: flowOf(null) }
.distinctUntilChangedBy { (it as? DataResult.Success)?.value?.keys }
.transformLatest { result ->
if (result == null || result !is DataResult.Success || result.value == null) {
emit(PublicAddressState.Error.NoPrimaryAccount)

View File

@ -21,9 +21,10 @@ import studio.forface.easygradle.dsl.android.*
plugins {
`java-library`
kotlin("jvm")
kotlin("plugin.serialization")
}
libVersion = Version(1, 16, 0)
libVersion = Version(1, 18, 0)
dependencies {
@ -33,7 +34,8 @@ dependencies {
// Kotlin
`kotlin-jdk7`,
`coroutines-core`,
`javax-inject`
`javax-inject`,
`serialization-json`,
)
testImplementation(project(Module.kotlinTest))

View File

@ -18,6 +18,9 @@
package me.proton.core.domain.entity
import kotlinx.serialization.Serializable
@Serializable
data class UserId(val id: String)
/**

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
import studio.forface.easygradle.dsl.*
import studio.forface.easygradle.dsl.android.*
plugins {
id("com.android.library")
kotlin("android")
}
libVersion = Version(1, 18, 0)
android()
dependencies {
api(
project(Module.eventManagerDomain),
project(Module.eventManagerData)
)
}

View File

@ -0,0 +1,73 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
import studio.forface.easygradle.dsl.*
import studio.forface.easygradle.dsl.android.*
plugins {
id("com.android.library")
kotlin("android")
kotlin("plugin.serialization")
kotlin("kapt")
id("dagger.hilt.android.plugin")
}
libVersion = parent?.libVersion
android()
dependencies {
implementation(
project(Module.kotlinUtil),
project(Module.data),
project(Module.dataRoom),
project(Module.domain),
project(Module.network),
project(Module.eventManagerDomain),
project(Module.presentation),
project(Module.account),
project(Module.accountManager),
project(Module.user), // UserEntity
`android-work-runtime`,
`hilt-android`,
`hilt-androidx-workManager`,
`kotlin-jdk7`,
`serialization-json`,
`coroutines-core`,
`retrofit`,
`retrofit-kotlin-serialization`,
`room-ktx`
)
kapt(
`hilt-android-compiler`,
`hilt-androidx-compiler`
)
testImplementation(
project(Module.androidTest),
project(Module.crypto),
project(Module.account),
project(Module.accountManager),
project(Module.contact),
project(Module.key),
)
androidTestImplementation(project(Module.androidInstrumentedTest))
}

View File

@ -0,0 +1,18 @@
<!--
~ Copyright (c) 2021 Proton Technologies AG
~ This file is part of Proton Technologies AG and ProtonCore.
~
~ ProtonCore 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.
~
~ ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
-->
<manifest package="me.proton.core.eventmanager.data" />

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data
import androidx.lifecycle.Lifecycle
import me.proton.core.accountmanager.domain.AccountManager
import me.proton.core.accountmanager.presentation.observe
import me.proton.core.accountmanager.presentation.onAccountReady
import me.proton.core.eventmanager.domain.EventManagerConfig
import me.proton.core.eventmanager.domain.EventManagerProvider
import me.proton.core.presentation.app.AppLifecycleProvider
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class CoreEventManagerStarter @Inject constructor(
private val appLifecycleProvider: AppLifecycleProvider,
private val accountManager: AccountManager,
private val eventManagerProvider: EventManagerProvider
) {
private fun startReadyAccount() {
accountManager.observe(appLifecycleProvider.lifecycle, minActiveState = Lifecycle.State.CREATED)
.onAccountReady { eventManagerProvider.get(EventManagerConfig.Core(it.userId)).start() }
}
fun start() {
startReadyAccount()
}
}

View File

@ -0,0 +1,113 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data
import me.proton.core.domain.entity.UserId
import me.proton.core.eventmanager.data.api.response.GetCalendarEventsResponse
import me.proton.core.eventmanager.data.api.response.GetCalendarLatestEventIdResponse
import me.proton.core.eventmanager.data.api.response.GetCoreEventsResponse
import me.proton.core.eventmanager.data.api.response.GetCoreLatestEventIdResponse
import me.proton.core.eventmanager.data.api.response.GetDriveEventsResponse
import me.proton.core.eventmanager.data.api.response.GetDriveLatestEventIdResponse
import me.proton.core.eventmanager.domain.EventManagerConfig
import me.proton.core.eventmanager.domain.entity.EventId
import me.proton.core.eventmanager.domain.entity.EventIdResponse
import me.proton.core.eventmanager.domain.entity.EventMetadata
import me.proton.core.eventmanager.domain.entity.EventsResponse
import me.proton.core.eventmanager.domain.entity.RefreshType
import me.proton.core.util.kotlin.deserialize
interface EventDeserializer {
val config: EventManagerConfig
val endpoint: String
fun deserializeLatestEventId(response: EventIdResponse): EventId
fun deserializeEventMetadata(userId: UserId, eventId: EventId, response: EventsResponse): EventMetadata
}
internal data class CoreEventDeserializer(
override val config: EventManagerConfig.Core
) : EventDeserializer {
override val endpoint = "events"
override fun deserializeLatestEventId(response: EventIdResponse): EventId =
EventId(response.body.deserialize<GetCoreLatestEventIdResponse>().eventId)
override fun deserializeEventMetadata(userId: UserId, eventId: EventId, response: EventsResponse): EventMetadata =
response.body.deserialize<GetCoreEventsResponse>().let {
EventMetadata(
userId = userId,
eventId = eventId,
config = config,
nextEventId = EventId(it.eventId),
refresh = RefreshType.map[it.refresh],
more = it.more > 0,
response = response,
createdAt = System.currentTimeMillis()
)
}
}
internal data class CalendarEventDeserializer(
override val config: EventManagerConfig.Calendar
) : EventDeserializer {
override val endpoint = "calendar/${config.apiVersion}/${config.calendarId}/modelevents"
override fun deserializeLatestEventId(response: EventIdResponse): EventId =
EventId(response.body.deserialize<GetCalendarLatestEventIdResponse>().eventId)
override fun deserializeEventMetadata(userId: UserId, eventId: EventId, response: EventsResponse): EventMetadata =
response.body.deserialize<GetCalendarEventsResponse>().let {
EventMetadata(
userId = userId,
eventId = eventId,
config = config,
nextEventId = EventId(it.eventId),
refresh = RefreshType.map[it.refresh],
more = it.more > 0,
response = response,
createdAt = System.currentTimeMillis()
)
}
}
internal class DriveEventDeserializer(
override val config: EventManagerConfig.Drive
) : EventDeserializer {
override val endpoint = "drive/shares/${config.shareId}/events"
override fun deserializeLatestEventId(response: EventIdResponse): EventId =
EventId(response.body.deserialize<GetDriveLatestEventIdResponse>().eventId)
override fun deserializeEventMetadata(userId: UserId, eventId: EventId, response: EventsResponse): EventMetadata =
response.body.deserialize<GetDriveEventsResponse>().let {
EventMetadata(
userId = userId,
eventId = eventId,
config = config,
nextEventId = EventId(it.eventId),
refresh = RefreshType.Nothing,
more = false,
response = response,
createdAt = System.currentTimeMillis()
)
}
}

View File

@ -0,0 +1,346 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.SerializationException
import me.proton.core.account.domain.entity.Account
import me.proton.core.account.domain.entity.AccountState
import me.proton.core.accountmanager.domain.AccountManager
import me.proton.core.eventmanager.data.extension.runCatching
import me.proton.core.eventmanager.data.extension.runInTransaction
import me.proton.core.eventmanager.domain.EventListener
import me.proton.core.eventmanager.domain.EventManager
import me.proton.core.eventmanager.domain.EventManagerConfig
import me.proton.core.eventmanager.domain.entity.Event
import me.proton.core.eventmanager.domain.entity.EventId
import me.proton.core.eventmanager.domain.entity.EventMetadata
import me.proton.core.eventmanager.domain.entity.EventsResponse
import me.proton.core.eventmanager.domain.entity.RefreshType
import me.proton.core.eventmanager.domain.entity.State
import me.proton.core.eventmanager.domain.repository.EventMetadataRepository
import me.proton.core.eventmanager.domain.work.EventWorkerManager
import me.proton.core.network.domain.ApiException
import me.proton.core.network.domain.isRetryable
import me.proton.core.presentation.app.AppLifecycleProvider
import me.proton.core.util.kotlin.CoreLogger
import me.proton.core.util.kotlin.exhaustive
@AssistedFactory
interface EventManagerFactory {
fun create(deserializer: EventDeserializer): EventManagerImpl
}
class EventManagerImpl @AssistedInject constructor(
@EventManagerCoroutineScope private val coroutineScope: CoroutineScope,
private val appLifecycleProvider: AppLifecycleProvider,
private val accountManager: AccountManager,
private val eventWorkerManager: EventWorkerManager,
internal val eventMetadataRepository: EventMetadataRepository,
@Assisted val deserializer: EventDeserializer
) : EventManager {
private val lock = Mutex()
private var observeAccountJob: Job? = null
private var observeAppStateJob: Job? = null
internal val eventListenersByOrder = sortedMapOf<Int, MutableSet<EventListener<*, *>>>()
private suspend fun deserializeEventsByListener(
response: EventsResponse
): Map<EventListener<*, *>, List<Event<*, *>>> {
return eventListenersByOrder.values.flatten().associateWith { eventListener ->
eventListener.deserializeEvents(response).orEmpty()
}
}
private suspend fun processFirstFromConfig() {
val metadata = eventMetadataRepository.get(config).firstOrNull() ?: return
when {
metadata.retry > retriesBeforeReset -> {
reportFailure(metadata)
reset()
}
metadata.retry > retriesBeforeNotifyResetAll -> {
reportFailure(metadata)
notifyResetAll(metadata)
}
else -> when (metadata.state) {
State.Enqueued -> fetch(metadata)
State.Fetching -> fetch(metadata)
State.Persisted -> notify(metadata)
State.NotifyPrepare -> notifyPrepare(metadata)
State.NotifyEvents -> notifyPrepare(metadata)
State.NotifyResetAll -> notifyResetAll(metadata)
State.NotifyComplete -> notifyComplete(metadata)
State.Completed -> Unit
}
}
}
private suspend fun reportFailure(metadata: EventMetadata) {
val list = eventMetadataRepository.get(config).map { it.copy(response = null) }
CoreLogger.log(LogTag.REPORT_MAX_RETRY, "Max Failure reached (current: ${metadata.eventId}): $list")
}
private suspend fun reset() {
eventMetadataRepository.deleteAll(config)
enqueue(eventId = null, immediately = true)
}
private suspend fun fetch(metadata: EventMetadata) {
val eventId = metadata.eventId ?: getLatestEventId()
runCatching(
config = config,
eventId = eventId,
processingState = State.Fetching,
successState = State.Persisted,
failureState = State.Enqueued
) {
val response = getEventResponse(eventId)
val deserializedMetadata = deserializeEventMetadata(eventId, response)
eventMetadataRepository.update(deserializedMetadata)
deserializedMetadata
}.onFailure {
when {
it is ApiException && it.isRetryable().not() -> notifyResetAll(metadata)
it is SerializationException -> notifyResetAll(metadata)
else -> throw it // Let's use the WorkManager RETRY mechanism (backoff + network constraint).
}
}.onSuccess {
notify(it)
}
}
private suspend fun notify(metadata: EventMetadata) {
when (metadata.refresh) {
RefreshType.Nothing -> notifyPrepare(metadata)
RefreshType.All,
RefreshType.Mail,
RefreshType.Contact -> notifyResetAll(metadata)
else -> notifyResetAll(metadata)
}.exhaustive
}
private suspend fun notifyResetAll(metadata: EventMetadata) {
runCatching(
config = config,
eventId = requireNotNull(metadata.eventId),
processingState = State.NotifyResetAll,
successState = State.NotifyComplete,
failureState = State.NotifyResetAll
) {
// Fully sequential and ordered.
eventListenersByOrder.values.flatten().forEach {
it.notifyResetAll(metadata.userId)
}
}.onFailure {
CoreLogger.e(LogTag.NOTIFY_ERROR, it)
enqueue(requireNotNull(metadata.eventId), immediately = true)
}.onSuccess {
notifyComplete(metadata)
}
}
private suspend fun notifyPrepare(metadata: EventMetadata) {
runCatching(
config = config,
eventId = requireNotNull(metadata.eventId),
processingState = State.NotifyPrepare,
successState = State.NotifyEvents,
failureState = State.NotifyPrepare
) {
// Set actions for all listeners.
val eventsByListener = deserializeEventsByListener(requireNotNull(metadata.response))
eventsByListener.forEach { (eventListener, list) ->
eventListener.setActionMap(metadata.userId, list as List<Nothing>)
}
// Notify prepare for all listeners.
eventListenersByOrder.values.flatten().forEach { eventListener ->
eventListener.notifyPrepare(metadata.userId)
}
}.onFailure {
CoreLogger.e(LogTag.NOTIFY_ERROR, it)
enqueue(metadata.eventId, immediately = true)
}.onSuccess {
notifyEvents(metadata)
}
}
private suspend fun notifyEvents(metadata: EventMetadata) {
runInTransaction(
config = config,
eventId = requireNotNull(metadata.eventId),
processingState = State.NotifyEvents,
successState = State.NotifyComplete,
failureState = State.NotifyPrepare
) {
// Fully sequential and ordered.
eventListenersByOrder.values.flatten().forEach { eventListener ->
eventListener.notifyEvents(metadata.userId)
}
}.onFailure {
CoreLogger.e(LogTag.NOTIFY_ERROR, it)
enqueue(metadata.eventId, immediately = true)
}.onSuccess {
notifyComplete(metadata)
}
}
private suspend fun notifyComplete(metadata: EventMetadata) {
runCatching(
config = config,
eventId = requireNotNull(metadata.eventId),
processingState = State.NotifyComplete,
successState = State.Completed,
failureState = State.Completed
) {
// Fully sequential and ordered.
eventListenersByOrder.values.flatten().forEach { eventListener ->
eventListener.notifyComplete(metadata.userId)
}
}.onFailure {
CoreLogger.e(LogTag.NOTIFY_ERROR, it)
enqueue(metadata.nextEventId, immediately = metadata.more ?: false)
}.onSuccess {
enqueue(metadata.nextEventId, immediately = metadata.more ?: false)
}
}
private suspend fun enqueue(eventId: EventId?, immediately: Boolean) {
val metadata = eventId?.let { eventMetadataRepository.get(config, it) }
eventMetadataRepository.update(
metadata?.takeUnless { metadata.eventId == metadata.nextEventId }?.copy(
retry = metadata.retry.plus(1)
) ?: EventMetadata(
userId = config.userId,
eventId = eventId,
config = config,
retry = 0,
state = State.Enqueued,
createdAt = System.currentTimeMillis()
)
)
eventWorkerManager.enqueue(config, immediately)
}
private suspend fun enqueueOrCancel(account: Account?) {
when {
account == null || account.userId != config.userId -> cancel()
account.state != AccountState.Ready -> cancel()
eventMetadataRepository.get(config).isEmpty() -> enqueue(eventId = null, immediately = true)
else -> eventWorkerManager.enqueue(config, immediately = true)
}
}
private suspend fun cancel() {
eventWorkerManager.cancel(config)
eventMetadataRepository.deleteAll(config)
}
private suspend fun internalStart() {
if (isStarted) return
// Observe any Account changes.
observeAccountJob = accountManager.getAccount(config.userId)
.distinctUntilChangedBy { it?.state }
.onEach { account -> enqueueOrCancel(account) }
.launchIn(coroutineScope)
// Observe any Foreground App State changes.
observeAppStateJob = appLifecycleProvider.state
.filter { it == AppLifecycleProvider.State.Foreground }
.onEach { enqueueOrCancel(accountManager.getAccount(config.userId).firstOrNull()) }
.launchIn(coroutineScope)
isStarted = true
}
private suspend fun internalStop() {
if (!isStarted) return
observeAccountJob?.cancel()
observeAppStateJob?.cancel()
eventWorkerManager.cancel(config)
isStarted = false
}
private suspend fun <R> internalSuspend(block: suspend () -> R): R {
return if (!isStarted) {
block.invoke()
} else {
internalStop()
try {
block.invoke()
} finally {
internalStart()
}
}
}
override val config: EventManagerConfig = deserializer.config
override var isStarted: Boolean = false
override suspend fun start() {
lock.withLock { internalStart() }
}
override suspend fun stop() {
lock.withLock { internalStop() }
}
override suspend fun <R> suspend(block: suspend () -> R): R {
lock.withLock { return internalSuspend(block) }
}
override fun subscribe(eventListener: EventListener<*, *>) {
eventListenersByOrder.getOrPut(eventListener.order) { mutableSetOf() }.add(eventListener)
}
override suspend fun process() = processFirstFromConfig()
override suspend fun getLatestEventId(): EventId =
eventMetadataRepository.getLatestEventId(config.userId, deserializer.endpoint)
.let { deserializer.deserializeLatestEventId(it) }
override suspend fun getEventResponse(eventId: EventId): EventsResponse =
eventMetadataRepository.getEvents(config.userId, eventId, deserializer.endpoint)
override suspend fun deserializeEventMetadata(eventId: EventId, response: EventsResponse): EventMetadata =
deserializer.deserializeEventMetadata(config.userId, eventId, response)
companion object {
// Constraint: retriesBeforeNotifyResetAll < retriesBeforeReset.
const val retriesBeforeNotifyResetAll = 3
const val retriesBeforeReset = 6
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data
import me.proton.core.eventmanager.domain.EventListener
import me.proton.core.eventmanager.domain.EventManager
import me.proton.core.eventmanager.domain.EventManagerConfig
import me.proton.core.eventmanager.domain.EventManagerProvider
import javax.inject.Singleton
@Singleton
class EventManagerProviderImpl(
private val eventManagerFactory: EventManagerFactory,
@JvmSuppressWildcards
private val eventListeners: Set<EventListener<*, *>>
) : EventManagerProvider {
// 1 EventManager instance per Config.
private val managers = mutableMapOf<EventManagerConfig, EventManager>()
private val eventListenersByType = eventListeners.groupBy { it.type }
override fun get(config: EventManagerConfig): EventManager {
// Only create a new instance if config is not found.
return managers.getOrPut(config) {
val deserializer = when (config) {
is EventManagerConfig.Core -> CoreEventDeserializer(config)
is EventManagerConfig.Calendar -> CalendarEventDeserializer(config)
is EventManagerConfig.Drive -> DriveEventDeserializer(config)
}
// Create a new EventManager associated with this config.
eventManagerFactory.create(deserializer).apply {
// Subscribe all known Listener for the same type to it.
eventListenersByType[config.listenerType]?.forEach { subscribe(it) }
}
}
}
override fun getAll(): List<EventManager> {
return managers.values.toList()
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data
import me.proton.core.util.kotlin.LoggerLogTag
object LogTag {
/** Tag for Worker Errors. */
const val WORKER_ERROR = "core.eventmanager.worker"
/** Tag for Notify Errors. */
const val NOTIFY_ERROR = "core.eventmanager.notify"
/** Tag for Max Retry Reports. */
val REPORT_MAX_RETRY = LoggerLogTag("core.eventmanager.report.maxretry")
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class EventManagerCoroutineScope

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data.api
import me.proton.core.network.data.protonApi.BaseRetrofitApi
import okhttp3.ResponseBody
import retrofit2.http.GET
import retrofit2.http.Path
interface EventApi : BaseRetrofitApi {
@GET("{endpoint}/latest")
suspend fun getLatestEventId(
@Path("endpoint", encoded = true) endpoint: String
): ResponseBody
@GET("{endpoint}/{eventId}")
suspend fun getEvents(
@Path("endpoint", encoded = true) endpoint: String,
@Path("eventId") eventId: String
): ResponseBody
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data.api.response
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetCalendarEventsResponse(
@SerialName("CalendarModelEventID")
val eventId: String,
@SerialName("Refresh")
val refresh: Int, // Bitmask to know what to refresh, 0: Nothing, 1: MAIL, 2: CONTACTS, 255: Everything.
@SerialName("More")
val more: Int, // 1 if there is more to poll.
)

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data.api.response
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetCalendarLatestEventIdResponse(
@SerialName("CalendarModelEventID")
val eventId: String
)

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data.api.response
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetCoreEventsResponse(
@SerialName("EventID")
val eventId: String,
@SerialName("Refresh")
val refresh: Int, // Bitmask to know what to refresh, 0: Nothing, 1: MAIL, 2: CONTACTS, 255: Everything.
@SerialName("More")
val more: Int, // 1 if there is more to poll.
)

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data.api.response
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetCoreLatestEventIdResponse(
@SerialName("EventID")
val eventId: String
)

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data.api.response
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetDriveEventsResponse(
@SerialName("EventID")
val eventId: String
)

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data.api.response
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetDriveLatestEventIdResponse(
@SerialName("EventID")
val eventId: String
)

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data.db
import androidx.room.TypeConverter
import me.proton.core.eventmanager.domain.EventManagerConfig
import me.proton.core.util.kotlin.deserialize
import me.proton.core.util.kotlin.serialize
class EventManagerConverters {
@TypeConverter
fun fromEventManagerConfigToString(value: EventManagerConfig?) = value?.serialize()
@TypeConverter
fun fromStringToEventManagerConfig(value: String?): EventManagerConfig? = value?.deserialize()
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data.db
import androidx.sqlite.db.SupportSQLiteDatabase
import me.proton.core.data.room.db.Database
import me.proton.core.data.room.db.migration.DatabaseMigration
import me.proton.core.eventmanager.data.db.dao.EventMetadataDao
interface EventMetadataDatabase : Database {
fun eventMetadataDao(): EventMetadataDao
companion object {
/**
* - Added Table EventMetadataEntity.
*/
val MIGRATION_0 = object : DatabaseMigration {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `EventMetadataEntity` (`userId` TEXT NOT NULL, `config` TEXT NOT NULL, `eventId` TEXT, `nextEventId` TEXT, `refresh` TEXT, `more` INTEGER, `response` TEXT, `retry` INTEGER NOT NULL, `state` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER, PRIMARY KEY(`userId`, `config`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )")
database.execSQL("CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_userId` ON `EventMetadataEntity` (`userId`)")
database.execSQL("CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_config` ON `EventMetadataEntity` (`config`)")
database.execSQL("CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_createdAt` ON `EventMetadataEntity` (`createdAt`)")
}
}
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data.db.dao
import androidx.room.Dao
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
import me.proton.core.data.room.db.BaseDao
import me.proton.core.domain.entity.UserId
import me.proton.core.eventmanager.data.entity.EventMetadataEntity
import me.proton.core.eventmanager.domain.EventManagerConfig
import me.proton.core.eventmanager.domain.entity.State
@Dao
abstract class EventMetadataDao : BaseDao<EventMetadataEntity>() {
@Query("SELECT * FROM EventMetadataEntity WHERE config = :config AND userId = :userId ORDER BY createdAt")
abstract fun observe(userId: UserId, config: EventManagerConfig): Flow<List<EventMetadataEntity>>
@Query("SELECT * FROM EventMetadataEntity WHERE config = :config AND userId = :userId AND eventId = :eventId")
abstract fun observe(userId: UserId, config: EventManagerConfig, eventId: String): Flow<EventMetadataEntity?>
@Query("SELECT * FROM EventMetadataEntity WHERE config = :config AND userId = :userId ORDER BY createdAt")
abstract suspend fun get(userId: UserId, config: EventManagerConfig): List<EventMetadataEntity>
@Query("SELECT * FROM EventMetadataEntity WHERE config = :config AND userId = :userId AND eventId = :eventId")
abstract suspend fun get(userId: UserId, config: EventManagerConfig, eventId: String): EventMetadataEntity?
@Query("UPDATE EventMetadataEntity SET state = :state, updatedAt = :updatedAt WHERE config = :config AND userId = :userId AND eventId = :eventId")
abstract suspend fun updateState(userId: UserId, config: EventManagerConfig, eventId: String, state: State, updatedAt: Long)
@Query("DELETE FROM EventMetadataEntity WHERE config = :config AND userId = :userId AND eventId = :eventId")
abstract suspend fun delete(userId: UserId, config: EventManagerConfig, eventId: String)
@Query("DELETE FROM EventMetadataEntity WHERE config = :config AND userId = :userId")
abstract suspend fun deleteAll(userId: UserId, config: EventManagerConfig)
}

View File

@ -0,0 +1,58 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data.entity
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import me.proton.core.domain.entity.UserId
import me.proton.core.eventmanager.domain.EventManagerConfig
import me.proton.core.eventmanager.domain.entity.RefreshType
import me.proton.core.eventmanager.domain.entity.State
import me.proton.core.user.data.entity.UserEntity
@Entity(
primaryKeys = ["userId", "config"],
indices = [
Index("userId"),
Index("config"),
Index("createdAt"),
],
foreignKeys = [
ForeignKey(
entity = UserEntity::class,
parentColumns = ["userId"],
childColumns = ["userId"],
onDelete = ForeignKey.CASCADE
)
]
)
data class EventMetadataEntity(
val userId: UserId,
val config: EventManagerConfig,
val eventId: String?,
val nextEventId: String?,
val refresh: RefreshType?,
val more: Boolean?,
val response: String?,
val retry: Int,
val state: State,
val createdAt: Long,
val updatedAt: Long?
)

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data.extension
import me.proton.core.eventmanager.domain.EventListener
suspend fun <R> Collection<EventListener<*, *>>.inTransaction(block: suspend () -> R): R {
return when {
isEmpty() -> block()
// Base condition.
count() == 1 -> last().inTransaction(block)
else -> first().inTransaction {
// Recursion.
drop(1).inTransaction(block)
}
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data.extension
import me.proton.core.eventmanager.data.EventManagerImpl
import me.proton.core.eventmanager.domain.EventManagerConfig
import me.proton.core.eventmanager.domain.entity.EventId
import me.proton.core.eventmanager.domain.entity.State
@SuppressWarnings("LongParameterList")
internal suspend fun <T> EventManagerImpl.runCatching(
config: EventManagerConfig,
eventId: EventId,
processingState: State,
successState: State? = null,
failureState: State? = null,
action: suspend () -> T
): Result<T> {
return runCatching {
eventMetadataRepository.updateState(config, eventId, processingState)
action.invoke()
}.onFailure {
failureState?.let { eventMetadataRepository.updateState(config, eventId, it) }
}.onSuccess {
successState?.let { eventMetadataRepository.updateState(config, eventId, it) }
}
}
@SuppressWarnings("LongParameterList")
internal suspend fun <T> EventManagerImpl.runInTransaction(
config: EventManagerConfig,
eventId: EventId,
processingState: State,
successState: State? = null,
failureState: State? = null,
action: suspend () -> T
): Result<T> {
return runCatching(
config = config,
eventId = eventId,
processingState = processingState,
failureState = failureState,
) {
eventListenersByOrder.values.flatten().inTransaction {
action.invoke().also {
successState?.let { eventMetadataRepository.updateState(config, eventId, it) }
}
}
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data.extension
import me.proton.core.eventmanager.data.entity.EventMetadataEntity
import me.proton.core.eventmanager.domain.entity.EventId
import me.proton.core.eventmanager.domain.entity.EventMetadata
import me.proton.core.eventmanager.domain.entity.EventsResponse
fun EventMetadata.toEntity() = EventMetadataEntity(
userId = userId,
config = config,
eventId = eventId?.id,
nextEventId = nextEventId?.id,
refresh = refresh,
more = more,
response = response?.body,
retry = retry,
state = state,
createdAt = createdAt,
updatedAt = updatedAt ?: System.currentTimeMillis()
)
fun EventMetadataEntity.fromEntity() = EventMetadata(
userId = userId,
config = config,
eventId = eventId?.let { EventId(it) },
nextEventId = nextEventId?.let { EventId(it) },
refresh = refresh,
more = more,
response = response?.let { EventsResponse(it) },
retry = retry,
state = state,
createdAt = createdAt,
updatedAt = updatedAt
)

View File

@ -0,0 +1,88 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data.repository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import me.proton.core.domain.entity.UserId
import me.proton.core.eventmanager.data.api.EventApi
import me.proton.core.eventmanager.data.db.EventMetadataDatabase
import me.proton.core.eventmanager.data.entity.EventMetadataEntity
import me.proton.core.eventmanager.data.extension.fromEntity
import me.proton.core.eventmanager.data.extension.toEntity
import me.proton.core.eventmanager.domain.EventManagerConfig
import me.proton.core.eventmanager.domain.entity.EventId
import me.proton.core.eventmanager.domain.entity.EventIdResponse
import me.proton.core.eventmanager.domain.entity.EventMetadata
import me.proton.core.eventmanager.domain.entity.EventsResponse
import me.proton.core.eventmanager.domain.entity.State
import me.proton.core.eventmanager.domain.repository.EventMetadataRepository
import me.proton.core.network.data.ApiProvider
open class EventMetadataRepositoryImpl(
db: EventMetadataDatabase,
private val provider: ApiProvider
) : EventMetadataRepository {
private val eventMetadataDao = db.eventMetadataDao()
private suspend fun insertOrUpdate(entity: EventMetadataEntity) =
eventMetadataDao.insertOrUpdate(entity)
override fun observe(config: EventManagerConfig): Flow<List<EventMetadata>> =
eventMetadataDao.observe(config.userId, config).map { it.map { entity -> entity.fromEntity() } }
override fun observe(config: EventManagerConfig, eventId: EventId): Flow<EventMetadata?> =
eventMetadataDao.observe(config.userId, config, eventId.id).map { entity -> entity?.fromEntity() }
override suspend fun delete(config: EventManagerConfig, eventId: EventId) =
eventMetadataDao.delete(config.userId, config, eventId.id)
override suspend fun deleteAll(config: EventManagerConfig) =
eventMetadataDao.deleteAll(config.userId, config)
override suspend fun update(metadata: EventMetadata) =
insertOrUpdate(metadata.toEntity().copy(updatedAt = System.currentTimeMillis()))
override suspend fun updateState(config: EventManagerConfig, eventId: EventId, state: State) =
eventMetadataDao.updateState(config.userId, config, eventId.id, state, System.currentTimeMillis())
override suspend fun get(config: EventManagerConfig): List<EventMetadata> =
eventMetadataDao.get(config.userId, config).map { it.fromEntity() }
override suspend fun get(config: EventManagerConfig, eventId: EventId): EventMetadata? =
eventMetadataDao.get(config.userId, config, eventId.id)?.fromEntity()
override suspend fun getLatestEventId(
userId: UserId,
endpoint: String
): EventIdResponse = provider.get<EventApi>(userId).invoke {
val response = getLatestEventId(endpoint)
EventIdResponse(response.string())
}.valueOrThrow
override suspend fun getEvents(
userId: UserId,
eventId: EventId,
endpoint: String
): EventsResponse = provider.get<EventApi>(userId).invoke {
val response = getEvents(endpoint, eventId.id)
EventsResponse(response.string())
}.valueOrThrow
}

View File

@ -0,0 +1,83 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data.work
import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import me.proton.core.eventmanager.data.LogTag
import me.proton.core.eventmanager.domain.EventManagerConfig
import me.proton.core.eventmanager.domain.EventManagerProvider
import me.proton.core.eventmanager.domain.work.EventWorkerManager
import me.proton.core.util.kotlin.CoreLogger
import me.proton.core.util.kotlin.deserialize
import me.proton.core.util.kotlin.serialize
import java.util.concurrent.TimeUnit
import kotlin.time.Duration
@HiltWorker
open class EventWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
private val eventManagerProvider: EventManagerProvider
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val config = requireNotNull(inputData.getString(KEY_INPUT_CONFIG)?.deserialize<EventManagerConfig>())
val manager = eventManagerProvider.get(config)
return runCatching { manager.process() }.fold(
onSuccess = {
Result.success()
},
onFailure = {
CoreLogger.e(LogTag.WORKER_ERROR, it)
Result.retry()
}
)
}
companion object {
private const val KEY_INPUT_CONFIG = "config"
fun getRequestFor(config: EventManagerConfig, initialDelay: Duration): PeriodicWorkRequest {
val initialDelaySeconds = initialDelay.inWholeSeconds
val backoffDelaySeconds = EventWorkerManager.BACKOFF_DELAY.inWholeSeconds
val repeatIntervalSeconds = EventWorkerManager.REPEAT_INTERVAL_BACKGROUND.inWholeSeconds
val serializedConfig = config.serialize()
return PeriodicWorkRequestBuilder<EventWorker>(repeatIntervalSeconds, TimeUnit.SECONDS)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, backoffDelaySeconds, TimeUnit.SECONDS)
.setInputData(workDataOf(KEY_INPUT_CONFIG to serializedConfig))
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
.setInitialDelay(initialDelaySeconds, TimeUnit.SECONDS)
.addTag(serializedConfig)
.addTag(config.listenerType.name)
.addTag(config.userId.id)
.build()
}
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data.work
import androidx.work.ExistingPeriodicWorkPolicy.REPLACE
import androidx.work.WorkManager
import me.proton.core.eventmanager.domain.EventManagerConfig
import me.proton.core.eventmanager.domain.work.EventWorkerManager
import me.proton.core.presentation.app.AppLifecycleProvider
import javax.inject.Inject
import kotlin.time.Duration
class EventWorkerManagerImpl @Inject constructor(
private val workManager: WorkManager,
private val appLifecycleProvider: AppLifecycleProvider
) : EventWorkerManager {
private fun getUniqueWorkName(config: EventManagerConfig) = "$config"
override fun enqueue(config: EventManagerConfig, immediately: Boolean) {
val uniqueWorkName = getUniqueWorkName(config)
val initialDelay = if (immediately) Duration.ZERO else when (appLifecycleProvider.state.value) {
AppLifecycleProvider.State.Background -> EventWorkerManager.REPEAT_INTERVAL_BACKGROUND
AppLifecycleProvider.State.Foreground -> EventWorkerManager.REPEAT_INTERVAL_FOREGROUND
}
val request = EventWorker.getRequestFor(config, initialDelay)
workManager.enqueueUniquePeriodicWork(uniqueWorkName, REPLACE, request)
}
override fun cancel(config: EventManagerConfig) {
val uniqueWorkName = getUniqueWorkName(config)
workManager.cancelUniqueWork(uniqueWorkName)
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data
import me.proton.core.domain.entity.UserId
import me.proton.core.eventmanager.domain.EventManagerConfig
import me.proton.core.eventmanager.domain.EventManagerProvider
import me.proton.core.eventmanager.domain.extension.suspend
class EventManagerApiGoal {
lateinit var provider: EventManagerProvider
suspend fun onAccountReady(userId: UserId) {
provider.get(EventManagerConfig.Core(userId)).start()
}
suspend fun onPerformAction(userId: UserId) {
suspend fun callApi() = Unit
provider.suspend(EventManagerConfig.Core(userId)) {
callApi()
}
}
suspend fun onCalendarActive(userId: UserId, calendarId: String) {
provider.get(EventManagerConfig.Calendar(userId, calendarId)).start()
}
suspend fun onCalendarInactive(userId: UserId, calendarId: String) {
provider.get(EventManagerConfig.Calendar(userId, calendarId)).stop()
}
suspend fun onDriveShareActive(userId: UserId, shareId: String) {
provider.get(EventManagerConfig.Drive(userId, shareId)).start()
}
suspend fun onDriveShareInactive(userId: UserId, shareId: String) {
provider.get(EventManagerConfig.Drive(userId, shareId)).stop()
}
}

View File

@ -0,0 +1,248 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.spyk
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineScope
import me.proton.core.account.domain.entity.Account
import me.proton.core.account.domain.entity.AccountDetails
import me.proton.core.account.domain.entity.AccountState
import me.proton.core.accountmanager.domain.AccountManager
import me.proton.core.domain.entity.UserId
import me.proton.core.eventmanager.data.listener.ContactEventListener
import me.proton.core.eventmanager.data.listener.UserEventListener
import me.proton.core.eventmanager.domain.EventListener
import me.proton.core.eventmanager.domain.EventManager
import me.proton.core.eventmanager.domain.EventManagerConfig
import me.proton.core.eventmanager.domain.EventManagerProvider
import me.proton.core.eventmanager.domain.entity.EventId
import me.proton.core.eventmanager.domain.entity.EventIdResponse
import me.proton.core.eventmanager.domain.entity.EventMetadata
import me.proton.core.eventmanager.domain.entity.EventsResponse
import me.proton.core.eventmanager.domain.entity.State
import me.proton.core.eventmanager.domain.repository.EventMetadataRepository
import me.proton.core.eventmanager.domain.work.EventWorkerManager
import me.proton.core.presentation.app.AppLifecycleProvider
import org.junit.Before
import org.junit.Test
class EventManagerImplTest {
private val coroutineScope = TestCoroutineScope()
private lateinit var eventManagerFactor: EventManagerFactory
private lateinit var eventManagerProvider: EventManagerProvider
private lateinit var appLifecycleProvider: AppLifecycleProvider
private lateinit var accountManager: AccountManager
private lateinit var eventWorkerManager: EventWorkerManager
private lateinit var eventMetadataRepository: EventMetadataRepository
private lateinit var userEventListener: UserEventListener
private lateinit var contactEventListener: ContactEventListener
private lateinit var listeners: Set<EventListener<*, *>>
private val user1 = Account(
userId = UserId("user1"),
username = "user1",
email = "user1@protonmail.com",
state = AccountState.Ready,
sessionId = null,
sessionState = null,
details = AccountDetails(null)
)
private val user2 = Account(
userId = UserId("user2"),
username = "user2",
email = "user2@protonmail.com",
state = AccountState.Ready,
sessionId = null,
sessionState = null,
details = AccountDetails(null)
)
private val accounts = listOf(user1, user2)
private val user1Config = EventManagerConfig.Core(user1.userId)
private val user2Config = EventManagerConfig.Core(user2.userId)
private val eventId = "eventId"
private val appState = MutableStateFlow(AppLifecycleProvider.State.Foreground)
private lateinit var user1Manager: EventManager
private lateinit var user2Manager: EventManager
@Before
fun before() {
userEventListener = spyk(UserEventListener())
contactEventListener = spyk(ContactEventListener())
listeners = setOf<EventListener<*, *>>(userEventListener, contactEventListener)
appLifecycleProvider = mockk {
every { state } returns appState
}
accountManager = mockk {
val userIdSlot = slot<UserId>()
every { getAccount(capture(userIdSlot)) } answers {
flowOf(accounts.firstOrNull { it.userId == userIdSlot.captured })
}
}
eventWorkerManager = spyk()
eventMetadataRepository = spyk()
eventManagerFactor = mockk {
val deserializerSlot = slot<EventDeserializer>()
every { create(capture(deserializerSlot)) } answers {
EventManagerImpl(
coroutineScope,
appLifecycleProvider,
accountManager,
eventWorkerManager,
eventMetadataRepository,
deserializerSlot.captured
)
}
}
eventManagerProvider = EventManagerProviderImpl(eventManagerFactor, listeners)
user1Manager = eventManagerProvider.get(user1Config)
user2Manager = eventManagerProvider.get(user2Config)
coEvery { eventMetadataRepository.getLatestEventId(any(), any()) } returns
EventIdResponse("{ \"EventID\": \"$eventId\" }")
coEvery { eventMetadataRepository.getEvents(any(), any(), any()) } returns
EventsResponse(TestEvents.coreFullEventsResponse)
coEvery { eventMetadataRepository.update(any()) } returns Unit
coEvery { eventMetadataRepository.updateState(any(), any(), any()) } returns Unit
// GIVEN
coEvery { eventMetadataRepository.get(user1Config) } returns
listOf(EventMetadata(user1.userId, EventId(eventId), user1Config, createdAt = 1))
coEvery { eventMetadataRepository.get(user2Config) } returns
listOf(EventMetadata(user2.userId, EventId(eventId), user2Config, createdAt = 1))
}
@Test
fun callCorrectPrepareUpdateDeleteCreate() = runBlocking {
// WHEN
user1Manager.process()
user2Manager.process()
// THEN
coVerify(exactly = 2) { userEventListener.inTransaction(any()) }
coVerify(exactly = 2) { contactEventListener.inTransaction(any()) }
coVerify(exactly = 1) { userEventListener.onPrepare(user1.userId, any()) }
coVerify(exactly = 1) { userEventListener.onUpdate(user1.userId, any()) }
coVerify(exactly = 0) { userEventListener.onDelete(user1.userId, any()) }
coVerify(exactly = 0) { userEventListener.onCreate(user1.userId, any()) }
coVerify(exactly = 0) { userEventListener.onPartial(user1.userId, any()) }
coVerify(exactly = 1) { contactEventListener.onPrepare(user1.userId, any()) }
coVerify(exactly = 0) { contactEventListener.onUpdate(user1.userId, any()) }
coVerify(exactly = 0) { contactEventListener.onDelete(user1.userId, any()) }
coVerify(exactly = 1) { contactEventListener.onCreate(user1.userId, any()) }
coVerify(exactly = 0) { contactEventListener.onPartial(user1.userId, any()) }
coVerify(atLeast = 1) { eventMetadataRepository.updateState(user1Config, any(), State.NotifyPrepare) }
coVerify(atLeast = 1) { eventMetadataRepository.updateState(user1Config, any(), State.NotifyEvents) }
coVerify(atLeast = 1) { eventMetadataRepository.updateState(user1Config, any(), State.NotifyComplete) }
coVerify(exactly = 1) { eventMetadataRepository.updateState(user1Config, any(), State.Completed) }
coVerify(exactly = 1) { userEventListener.onPrepare(user2.userId, any()) }
coVerify(exactly = 1) { userEventListener.onUpdate(user2.userId, any()) }
coVerify(exactly = 0) { userEventListener.onDelete(user2.userId, any()) }
coVerify(exactly = 0) { userEventListener.onCreate(user2.userId, any()) }
coVerify(exactly = 0) { userEventListener.onPartial(user2.userId, any()) }
coVerify(exactly = 1) { contactEventListener.onPrepare(user2.userId, any()) }
coVerify(exactly = 0) { contactEventListener.onUpdate(user2.userId, any()) }
coVerify(exactly = 0) { contactEventListener.onDelete(user2.userId, any()) }
coVerify(exactly = 1) { contactEventListener.onCreate(user2.userId, any()) }
coVerify(exactly = 0) { contactEventListener.onPartial(user2.userId, any()) }
coVerify(atLeast = 1) { eventMetadataRepository.updateState(user2Config, any(), State.NotifyPrepare) }
coVerify(atLeast = 1) { eventMetadataRepository.updateState(user2Config, any(), State.NotifyEvents) }
coVerify(atLeast = 1) { eventMetadataRepository.updateState(user2Config, any(), State.NotifyComplete) }
coVerify(exactly = 1) { eventMetadataRepository.updateState(user2Config, any(), State.Completed) }
}
@Test
fun callOnPrepareThrowException() = runBlocking {
// GIVEN
coEvery { userEventListener.onPrepare(user1.userId, any()) } throws Exception("IOException")
// WHEN
user1Manager.process()
// THEN
coVerify(exactly = 2) { eventMetadataRepository.updateState(any(), any(), State.NotifyPrepare) }
coVerify(exactly = 0) { userEventListener.onUpdate(user1.userId, any()) }
coVerify(exactly = 0) { userEventListener.inTransaction(any()) }
}
@Test
fun callOnUpdateThrowException() = runBlocking {
// GIVEN
coEvery { userEventListener.onUpdate(user1.userId, any()) } throws Exception("SqlForeignKeyException")
// WHEN
user1Manager.process()
// THEN
coVerify(exactly = 2) { eventMetadataRepository.updateState(any(), any(), State.NotifyPrepare) }
coVerify(exactly = 2) { eventMetadataRepository.updateState(any(), any(), State.NotifyEvents) }
coVerify(exactly = 1) { userEventListener.onUpdate(user1.userId, any()) }
coVerify(exactly = 1) { userEventListener.inTransaction(any()) }
}
@Test
fun preventMultiSubscribe() = runBlocking {
// GIVEN
user1Manager.subscribe(userEventListener)
user1Manager.subscribe(userEventListener)
user1Manager.subscribe(userEventListener)
// WHEN
user1Manager.process()
// THEN
coVerify(exactly = 1) { userEventListener.onPrepare(user1.userId, any()) }
coVerify(exactly = 1) { userEventListener.onUpdate(user1.userId, any()) }
coVerify(exactly = 0) { userEventListener.onDelete(user1.userId, any()) }
coVerify(exactly = 0) { userEventListener.onCreate(user1.userId, any()) }
coVerify(exactly = 0) { userEventListener.onPartial(user1.userId, any()) }
}
@Test
fun preventEventIfNoUser() = runBlocking {
// GIVEN
coEvery { eventMetadataRepository.get(user1Config) } returns emptyList()
// WHEN
user1Manager.process()
// THEN
coVerify(exactly = 0) { userEventListener.onPrepare(any(), any()) }
coVerify(exactly = 0) { userEventListener.onUpdate(any(), any()) }
coVerify(exactly = 0) { userEventListener.onDelete(any(), any()) }
coVerify(exactly = 0) { userEventListener.onCreate(any(), any()) }
coVerify(exactly = 0) { userEventListener.onPartial(any(), any()) }
}
}

View File

@ -0,0 +1,247 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data
object TestEvents {
val coreFullEventsResponse =
"""
{
"Code": 1000,
"EventID": "ACXDmTaBub14w==",
"Refresh": 0,
"More": 0,
"Contacts": [
{
"ID": "afeaefaeTaBub14w==",
"Action": 1,
"Contact": {
"ID": "a29olIjFv0rnXxBhSMw==",
"Name": "ProtonMail Features",
"ContactEmails": [
{
"ID": "aefew4323jFv0BhSMw==",
"Name": "test1",
"Email": "features@protonmail.black",
"Type": [
"work"
],
"Defaults": 1,
"Order": 1,
"ContactID": "a29olIjFv0rnXxBhSMw==",
"LabelIDs": [
"I6hgx3Ol-d3HYa3E394T_ACXDmTaBub14w=="
]
}
],
"LabelIDs": [
"I6hgx3Ol-d3HYa3E394T_ACXDmTaBub14w=="
],
"Cards": [
{
"Type": 2,
"Data": "BEGIN:VCARD\\r\\nVERSION:4.0\\r\\nFN:ProtonMail Features\\r\\nUID:proton-legacy-139892c2-f691-4118-8c29-061196013e04\\r\\nitem1.EMAIL;TYPE=work;PREF=1:features@protonmail.black\\r\\nitem2.EMAIL;TYPE=home;PREF=2:features@protonmail.ch\\r\\nEND:VCARD\\r\\n",
"Signature": "-----BEGIN PGP SIGNATURE-----.*-----END PGP SIGNATURE-----"
}
]
}
}
],
"ContactEmails": [
{
"ID": "sadfaACXDmTaBub14w==",
"Action": 1,
"ContactEmail": {
"ID": "aefew4323jFv0BhSMw==",
"Name": "test1",
"Email": "features@protonmail.black",
"Type": [
"work"
],
"Defaults": 1,
"Order": 1,
"ContactID": "a29olIjFv0rnXxBhSMw==",
"LabelIDs": [
"I6hgx3Ol-d3HYa3E394T_ACXDmTaBub14w=="
]
}
}
],
"User": {
"ID": "MJLke8kWh1BBvG95JBIrZvzpgsZ94hNNgjNHVyhXMiv4g9cn6SgvqiIFR5cigpml2LD_iUk_3DkV29oojTt3eA==",
"Name": "jason",
"UsedSpace": 96691332,
"Currency": "USD",
"Credit": 0,
"MaxSpace": 10737418240,
"MaxUpload": 26214400,
"Role": 2,
"Private": 1,
"ToMigrate": 1,
"MnemonicStatus": 1,
"Subscribed": 1,
"Services": 1,
"Delinquent": 0,
"Email": "jason@protonmail.ch",
"DisplayName": "Jason",
"Keys": [{
"ID": "IlnTbqicN-2HfUGIn-ki8bqZfLqNj5ErUB0z24Qx5g-4NvrrIc6GLvEpj2EPfwGDv28aKYVRRrSgEFhR_zhlkA==",
"Version": 3,
"PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----*-----END PGP PRIVATE KEY BLOCK-----",
"Fingerprint": "c93f767df53b0ca8395cfde90483475164ec6353",
"Activation": null,
"Primary": 1
}]
},
"UserSettings": {
"Email": {
"Value": "abc@gmail.com",
"Status": 0,
"Notify": 1,
"Reset": 0
},
"Phone": {
"Value": "+18005555555",
"Status": 0,
"Notify": 0,
"Reset": 0
},
"Password": {
"Mode": 2,
"ExpirationTime": null
},
"2FA": {
"Enabled": 3,
"Allowed": 3,
"ExpirationTime": null,
"U2FKeys": [
{
"Label": "A name",
"KeyHandle": "aKeyHandle",
"Compromised": 0
}
]
},
"News": 244,
"Locale": "en_US",
"LogAuth": 2,
"InvoiceText": "AnyText",
"Density": 0,
"Theme": "css",
"ThemeType": 1,
"WeekStart": 1,
"DateFormat": 1,
"TimeFormat": 1,
"Welcome": "1",
"WelcomeFlag": "1",
"EarlyAccess": "1",
"FontSize": "14",
"Flags": {
"Welcomed": 0
}
},
"MailSettings": {
"DisplayName": "Put Chinese Here",
"Signature": "This is my signature",
"Theme": "<CSS>",
"AutoResponder": {
"StartTime": 0,
"Endtime": 0,
"Repeat": 0,
"DaysSelected": [
"string"
],
"Subject": "Auto",
"Message": "",
"IsEnabled": null,
"Zone": "Europe/Zurich"
},
"AutoSaveContacts": 1,
"AutoWildcardSearch": 1,
"ComposerMode": 0,
"MessageButtons": 0,
"ShowImages": 2,
"ShowMoved": 0,
"ViewMode": 0,
"ViewLayout": 0,
"SwipeLeft": 3,
"SwipeRight": 0,
"AlsoArchive": 0,
"Hotkeys": 1,
"Shortcuts": 1,
"PMSignature": 0,
"ImageProxy": 0,
"NumMessagePerPage": 50,
"DraftMIMEType": "text/html",
"ReceiveMIMEType": "text/html",
"ShowMIMEType": "text/html",
"EnableFolderColor": 0,
"InheritParentFolderColor": 1,
"TLS": 0,
"RightToLeft": 0,
"AttachPublicKey": 0,
"Sign": 0,
"PGPScheme": 16,
"PromptPin": 0,
"Autocrypt": 0,
"StickyLabels": 0,
"ConfirmLink": 1,
"DelaySendSeconds": 10,
"KT": 0,
"FontSize": null,
"FontFace": null
},
"Addresses": [
{
"ID": "q_9v-GXEPLagg81jsUz2mHQ==",
"Action": 2,
"Address": {
"ID": "q_9v-GXEPLagg81jsUz2mHQ==",
"DomainID": "l8vWAXHBQmvzmKUA==",
"Email": "test@protonmail.com",
"Send": 0,
"Receive": 0,
"Status": 0,
"Type": 2,
"Order": 8,
"DisplayName": "Namey",
"Signature": "Sent from <a href=\"https://protonmail.ch\">ProtonMail</a>",
"HasKeys": 1,
"Keys": [
{
"ID": "a0f5_q7xkcyON1blZKTPxmBceURtzhW5Jc1rhtWUw5w2QXCMkSzHNustWtTjUlma9JmiL8O71aimfMOyY3UUGQ==",
"Version": 3,
"PublicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----...-----END PGP PUBLIC KEY BLOCK-----",
"PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----...-----END PGP PRIVATE KEY BLOCK-----",
"Token": "null or -----BEGIN PGP MESSAGE-----.*-----END PGP MESSAGE-----",
"Signature": "null or -----BEGIN PGP SIGNATURE-----.*-----END PGP SIGNATURE-----",
"Fingerprint": "e7e5466d21ff064ef870a7a393526f79e83004b0",
"Fingerprints": [
"e7e5466d21ff064ef870a7a393526f79e83004b0"
],
"Activation": null,
"Primary": 1
}
]
}
}
]
}
""".trimIndent()
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data.listener
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import me.proton.core.contact.data.api.resource.ContactWithCardsResource
import me.proton.core.eventmanager.domain.EventListener
import me.proton.core.eventmanager.domain.entity.Action
import me.proton.core.eventmanager.domain.entity.Event
import me.proton.core.eventmanager.domain.entity.EventsResponse
import me.proton.core.util.kotlin.deserializeOrNull
@Serializable
data class ContactsEvents(
@SerialName("Contacts")
val contacts: List<ContactEvent>
)
@Serializable
data class ContactEvent(
@SerialName("ID")
val id: String,
@SerialName("Action")
val action: Int,
@SerialName("Contact")
val contact: ContactWithCardsResource? = null
)
class ContactEventListener : EventListener<String, ContactWithCardsResource>() {
override val type = Type.Core
override val order = 1
override suspend fun deserializeEvents(response: EventsResponse): List<Event<String, ContactWithCardsResource>>? {
return response.body.deserializeOrNull<ContactsEvents>()?.contacts?.map {
Event(requireNotNull(Action.map[it.action]), it.id, it.contact)
}
}
override suspend fun <R> inTransaction(block: suspend () -> R): R {
// Db.inTransaction(block)
return block()
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.data.listener
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import me.proton.core.eventmanager.domain.EventListener
import me.proton.core.eventmanager.domain.entity.Action
import me.proton.core.eventmanager.domain.entity.Event
import me.proton.core.eventmanager.domain.entity.EventsResponse
import me.proton.core.key.data.api.response.UserResponse
import me.proton.core.util.kotlin.deserializeOrNull
@Serializable
data class UserEvents(
@SerialName("User")
val user: UserResponse
)
class UserEventListener : EventListener<String, UserResponse>() {
override val type = Type.Core
override val order = 0
override suspend fun deserializeEvents(response: EventsResponse): List<Event<String, UserResponse>>? {
return response.body.deserializeOrNull<UserEvents>()?.let {
listOf(Event(Action.Update, it.user.id, it.user))
}
}
override suspend fun <R> inTransaction(block: suspend () -> R): R {
// Db.inTransaction(block)
return block()
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
import studio.forface.easygradle.dsl.*
import studio.forface.easygradle.dsl.android.*
plugins {
`java-library`
kotlin("jvm")
kotlin("plugin.serialization")
}
libVersion = parent?.libVersion
dependencies {
implementation(
project(Module.kotlinUtil),
project(Module.domain),
project(Module.cryptoCommon),
project(Module.networkDomain),
// Kotlin
`kotlin-jdk8`,
`serialization-json`,
`coroutines-core`
)
testImplementation(project(Module.kotlinTest))
}

View File

@ -0,0 +1,202 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.domain
import me.proton.core.domain.entity.UserId
import me.proton.core.eventmanager.domain.entity.Action
import me.proton.core.eventmanager.domain.entity.Event
import me.proton.core.eventmanager.domain.entity.EventsResponse
import me.proton.core.eventmanager.domain.extension.groupByAction
import me.proton.core.util.kotlin.takeIfNotEmpty
abstract class EventListener<K, T : Any> : TransactionHandler {
private val actionMapByUserId = mutableMapOf<UserId, Map<Action, List<Event<K, T>>>>()
/**
* Type of Event loop.
*/
enum class Type {
/**
* Core Event loop.
*
* Contains: Messages, Conversations, Import, Contacts, Filter, Labels, Subscriptions, User, Settings, ...
*/
Core,
/**
* Calendar Event loop.
*
* Contains: Calendars, CalendarKeys, CalendarEvents, CalendarAlarms, Settings, CalendarSubscriptions, ...
*/
Calendar,
/**
* Drive Event loop.
*
* Contains: Share, Links, Nodes, ...
*/
Drive
}
/**
* Listener [type] to associate with this [EventListener].
*/
abstract val type: Type
/**
* The degrees of separation from this entity to the User entity.
*
* Examples:
* - UserEventListener: User => 0.
* - UserAddressEventListener: UserAddress -> User => 1.
* - ContactEventListener: Contact -> User => 1.
* - ContactEmailEventListener: ContactEmail -> Contact -> User => 2.
*/
abstract val order: Int
/**
* Get actions part of the current set of modifications.
*
* Note: The map is created just before [onPrepare], and cleared after [onComplete].
*/
fun getActionMap(userId: UserId): Map<Action, List<Event<K, T>>> {
return actionMapByUserId.getOrPut(userId) { emptyMap() }
}
/**
* Set the actions part of the current set of modifications.
*
* Note: Called before [notifyPrepare] and after [notifyComplete].
*/
fun setActionMap(userId: UserId, events: List<Event<K, T>>) {
actionMapByUserId[userId] = events.groupByAction()
}
/**
* Notify to prepare any additional data (e.g. foreign key).
*
* Note: No transaction wraps this function.
*/
suspend fun notifyPrepare(userId: UserId) {
val actions = getActionMap(userId)
val entities = actions[Action.Create].orEmpty() + actions[Action.Update].orEmpty()
entities.takeIfNotEmpty()?.let { list -> onPrepare(userId, list.mapNotNull { it.entity }) }
}
/**
* Notify all events in this order [onCreate], [onUpdate], [onPartial] and [onDelete].
*
* Note: A transaction wraps this function.
*/
suspend fun notifyEvents(userId: UserId) {
val actions = getActionMap(userId)
actions[Action.Create]?.takeIfNotEmpty()?.let { list -> onCreate(userId, list.mapNotNull { it.entity }) }
actions[Action.Update]?.takeIfNotEmpty()?.let { list -> onUpdate(userId, list.mapNotNull { it.entity }) }
actions[Action.Partial]?.takeIfNotEmpty()?.let { list -> onPartial(userId, list.mapNotNull { it.entity }) }
actions[Action.Delete]?.takeIfNotEmpty()?.let { list -> onDelete(userId, list.map { it.key }) }
}
/**
* Notify to reset all entities.
*
* Note: No transaction wraps this function.
*/
suspend fun notifyResetAll(userId: UserId) {
onResetAll(userId)
}
/**
* Notify complete, whether set of modifications was successful or not.
*
* Note: No transaction wraps this function.
*/
suspend fun notifyComplete(userId: UserId) {
onComplete(userId)
setActionMap(userId, emptyList())
}
/**
* Deserialize [response] into a typed list of [Event].
*/
abstract suspend fun deserializeEvents(response: EventsResponse): List<Event<K, T>>?
/**
* Called before applying a set of modifications to prepare any additional action (e.g. fetch foreign entities).
*
* Note: Delete action entities are filtered out.
*
* @see onComplete
*/
open suspend fun onPrepare(userId: UserId, entities: List<T>) = Unit
/**
* Called at the end of a set of modifications, whether it is successful or not.
*
* @see onPrepare
*/
open suspend fun onComplete(userId: UserId) = Unit
/**
* Called to created entities in persistence.
*
* There is no guarantee any prior [onCreate] will be called for any needed foreign entity.
*
* Note: A transaction wraps this function and must return as fast as possible.
*
* @see onPrepare
*/
open suspend fun onCreate(userId: UserId, entities: List<T>) = Unit
/**
* Called to update or insert entities in persistence.
*
* There is no guarantee any prior [onCreate] will be called for any needed foreign entity.
*
* Note: A transaction wraps this function and must return as fast as possible.
*
* @see onPrepare
*/
open suspend fun onUpdate(userId: UserId, entities: List<T>) = Unit
/**
* Called to partially update entities in persistence.
*
* There is no guarantee any prior [onCreate] will be called for any needed foreign entity.
*
* Note: A transaction wraps this function and must return as fast as possible.
*
* @see onPrepare
*/
open suspend fun onPartial(userId: UserId, entities: List<T>) = Unit
/**
* Called to delete, if exist, entities in persistence.
*
* Note: A transaction wraps this function and must return as fast as possible.
*/
open suspend fun onDelete(userId: UserId, keys: List<K>) = Unit
/**
* Called to reset/delete all entities in persistence, for this type.
*
* Note: Usually a good time the fetch minimal data after deletion.
*/
open suspend fun onResetAll(userId: UserId) = Unit
}

View File

@ -0,0 +1,85 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.domain
import me.proton.core.eventmanager.domain.entity.EventId
import me.proton.core.eventmanager.domain.entity.EventMetadata
import me.proton.core.eventmanager.domain.entity.EventsResponse
interface EventManager {
/**
* [EventManagerConfig] associated with this [EventManager].
*/
val config: EventManagerConfig
/**
* Returns `true` when the Event loop is started.
*
* @see start
* @see stop
*/
val isStarted: Boolean
/**
* Start the Event loop. This call can be called multiple consecutive time without affecting the behavior.
*
* Note: The loop will automatically be paused or resumed depending the associated [config] User account state.
*/
suspend fun start()
/**
* Stop the Event loop. This call can be called multiple consecutive time without affecting the behavior.
*
* Note: The loop will not automatically be restarted/resumed after this call.
*/
suspend fun stop()
/**
* Pause the Event loop, calls the specified function [block], and resume the loop.
*
* Note: The loop will not be paused/resumed if it was not already started.
*/
suspend fun <R> suspend(block: suspend () -> R): R
/**
* Subscribe a new [eventListener].
*/
fun subscribe(eventListener: EventListener<*, *>)
/**
* Process the next task for the associated [config], if exist.
*/
suspend fun process()
/**
* Fetch the latest EventId for the associated [config].
*/
suspend fun getLatestEventId(): EventId
/**
* Fetch the Events for the associated [config] and [eventId].
*/
suspend fun getEventResponse(eventId: EventId): EventsResponse
/**
* Deserialize an [EventMetadata] from [response] for the associated [config] and [eventId].
*/
suspend fun deserializeEventMetadata(eventId: EventId, response: EventsResponse): EventMetadata
}

View File

@ -0,0 +1,53 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.domain
import kotlinx.serialization.Serializable
import me.proton.core.domain.entity.UserId
@Serializable
sealed class EventManagerConfig {
abstract val listenerType: EventListener.Type
abstract val userId: UserId
@Serializable
data class Core(
override val userId: UserId
) : EventManagerConfig() {
override val listenerType = EventListener.Type.Core
}
@Serializable
data class Calendar(
override val userId: UserId,
val calendarId: String,
val apiVersion: String = "v1"
) : EventManagerConfig() {
override val listenerType = EventListener.Type.Calendar
}
@Serializable
data class Drive(
override val userId: UserId,
val shareId: String
) : EventManagerConfig() {
override val listenerType = EventListener.Type.Drive
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.domain
interface EventManagerProvider {
/**
* Get an [EventManager] associated with the given [config].
*/
fun get(config: EventManagerConfig): EventManager
/**
* Get all [EventManager].
*/
fun getAll(): List<EventManager>
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.domain
interface TransactionHandler {
/**
* Calls the specified suspending [block] in a transaction. The transaction will be marked as successful unless
* an exception is thrown in the suspending [block] or the coroutine is cancelled.
*/
suspend fun <R> inTransaction(block: suspend () -> R): R
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.domain.entity
data class Event<K, T>(
val action: Action,
val key: K,
val entity: T?
)
enum class Action(val value: Int) {
Delete(0),
Create(1),
Update(2),
Partial(3);
companion object {
val map = values().associateBy { it.value }
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.domain.entity
data class EventId(val id: String)

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.domain.entity
data class EventIdResponse(val body: String)

View File

@ -0,0 +1,58 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.domain.entity
import me.proton.core.domain.entity.UserId
import me.proton.core.eventmanager.domain.EventManagerConfig
data class EventMetadata(
val userId: UserId,
val eventId: EventId?,
val config: EventManagerConfig,
val nextEventId: EventId? = null,
val refresh: RefreshType? = null,
val more: Boolean? = null,
val response: EventsResponse? = null,
val retry: Int = 0,
val state: State = State.Enqueued,
val createdAt: Long,
val updatedAt: Long? = null
)
enum class RefreshType(val value: Int) {
Nothing(0),
Mail(1),
Contact(2),
All(255);
companion object {
val map = values().associateBy { it.value }
}
}
enum class State(val value: Int) {
Enqueued(0),
Fetching(1),
Persisted(2),
NotifyPrepare(3),
NotifyEvents(4),
NotifyResetAll(5),
NotifyComplete(6),
Completed(7),
}

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.domain.entity
data class EventsResponse(val body: String)

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.domain.extension
import me.proton.core.eventmanager.domain.entity.Action
import me.proton.core.eventmanager.domain.entity.Event
/**
* Group all [Event] by [Event.action].
*/
fun <K, T> List<Event<K, T>>.groupByAction(): Map<Action, List<Event<K, T>>> =
groupBy(keySelector = { event -> event.action }, valueTransform = { event -> event })

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.domain.extension
import me.proton.core.eventmanager.domain.EventManagerConfig
import me.proton.core.eventmanager.domain.EventManagerProvider
/**
* Pause the Event loop, calls the specified function [block], and resume the loop.
*
* Note: The loop will not be paused/resumed if it was not already started.
*/
suspend fun <R> EventManagerProvider.suspend(config: EventManagerConfig, block: suspend () -> R): R {
return get(config).suspend(block)
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.domain.repository
import kotlinx.coroutines.flow.Flow
import me.proton.core.domain.entity.UserId
import me.proton.core.eventmanager.domain.EventManagerConfig
import me.proton.core.eventmanager.domain.entity.EventId
import me.proton.core.eventmanager.domain.entity.EventIdResponse
import me.proton.core.eventmanager.domain.entity.EventMetadata
import me.proton.core.eventmanager.domain.entity.EventsResponse
import me.proton.core.eventmanager.domain.entity.State
interface EventMetadataRepository {
fun observe(config: EventManagerConfig): Flow<List<EventMetadata>>
fun observe(config: EventManagerConfig, eventId: EventId): Flow<EventMetadata?>
suspend fun deleteAll(config: EventManagerConfig)
suspend fun delete(config: EventManagerConfig, eventId: EventId)
suspend fun update(metadata: EventMetadata)
suspend fun updateState(config: EventManagerConfig, eventId: EventId, state: State)
suspend fun get(config: EventManagerConfig): List<EventMetadata>
suspend fun get(config: EventManagerConfig, eventId: EventId): EventMetadata?
suspend fun getLatestEventId(userId: UserId, endpoint: String): EventIdResponse
suspend fun getEvents(userId: UserId, eventId: EventId, endpoint: String): EventsResponse
}

View File

@ -0,0 +1,43 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.eventmanager.domain.work
import me.proton.core.eventmanager.domain.EventManagerConfig
import kotlin.time.Duration
interface EventWorkerManager {
/**
* Enqueue a Worker for this [config].
*
* @param immediately if true, start/process the task immediately, if possible.
*/
fun enqueue(config: EventManagerConfig, immediately: Boolean)
/**
* Cancel Worker(s) for this [config].
*/
fun cancel(config: EventManagerConfig)
companion object {
val REPEAT_INTERVAL_FOREGROUND = Duration.seconds(30)
val REPEAT_INTERVAL_BACKGROUND = Duration.minutes(30)
val BACKOFF_DELAY = REPEAT_INTERVAL_FOREGROUND
}
}

View File

@ -0,0 +1,18 @@
<!--
~ Copyright (c) 2021 Proton Technologies AG
~ This file is part of Proton Technologies AG and ProtonCore.
~
~ ProtonCore 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.
~
~ ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
-->
<manifest package="me.proton.core.eventmanager" />

View File

@ -77,6 +77,11 @@ object Module {
const val cryptoCommon = "$crypto:crypto-common"
const val cryptoAndroid = "$crypto:crypto-android"
// Account
const val eventManager = ":event-manager"
const val eventManagerDomain = "$eventManager:event-manager-domain"
const val eventManagerData = "$eventManager:event-manager-data"
// Key
const val key = ":key"
const val keyDomain = "$key:key-domain"

View File

@ -60,7 +60,6 @@ internal fun initVersions() {
// region Android
const val `android-tools version` = "30.0.2" // Updated: Jun, 2020
const val `androidUi version` = "0.1.0-dev08" // Released: Apr 03, 2020
// endregion
// region Other

View File

@ -23,7 +23,7 @@ plugins {
kotlin("android")
}
libVersion = Version(1, 15, 0)
libVersion = Version(1, 18, 0)
android()

View File

@ -24,7 +24,7 @@ plugins {
kotlin("plugin.serialization")
}
libVersion = Version(1, 15, 0)
libVersion = parent?.libVersion
android()
@ -38,6 +38,7 @@ dependencies {
project(Module.network),
project(Module.mailSettingsDomain),
project(Module.userData),
project(Module.eventManagerDomain),
// Kotlin
`kotlin-jdk7`,
@ -45,6 +46,7 @@ dependencies {
`coroutines-core`,
// Other
`hilt-android`,
`okHttp-logging`,
`retrofit`,
`retrofit-kotlin-serialization`,

View File

@ -0,0 +1,68 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.mailsettings.data
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import me.proton.core.domain.entity.UserId
import me.proton.core.eventmanager.domain.EventListener
import me.proton.core.eventmanager.domain.entity.Action
import me.proton.core.eventmanager.domain.entity.Event
import me.proton.core.eventmanager.domain.entity.EventsResponse
import me.proton.core.mailsettings.data.api.response.MailSettingsResponse
import me.proton.core.mailsettings.data.db.MailSettingsDatabase
import me.proton.core.mailsettings.data.extension.toMailSettings
import me.proton.core.mailsettings.domain.repository.MailSettingsRepository
import me.proton.core.util.kotlin.deserializeOrNull
import javax.inject.Inject
import javax.inject.Singleton
@Serializable
data class MailSettingsEvents(
@SerialName("MailSettings")
val settings: MailSettingsResponse
)
@Singleton
class MailSettingsEventListener @Inject constructor(
private val db: MailSettingsDatabase,
private val repository: MailSettingsRepository
) : EventListener<String, MailSettingsResponse>() {
override val type = Type.Core
override val order = 1
override suspend fun deserializeEvents(response: EventsResponse): List<Event<String, MailSettingsResponse>>? {
return response.body.deserializeOrNull<MailSettingsEvents>()?.let {
listOf(Event(Action.Update, "null", it.settings))
}
}
override suspend fun <R> inTransaction(block: suspend () -> R): R {
return db.inTransaction(block)
}
override suspend fun onUpdate(userId: UserId, entities: List<MailSettingsResponse>) {
repository.updateMailSettings(entities.first().toMailSettings(userId))
}
override suspend fun onResetAll(userId: UserId) {
repository.getMailSettings(userId, refresh = true)
}
}

View File

@ -23,7 +23,7 @@ plugins {
kotlin("jvm")
}
libVersion = Version(1, 15, 0)
libVersion = parent?.libVersion
dependencies {

View File

@ -26,7 +26,7 @@ plugins {
id("kotlin-parcelize")
}
libVersion = Version(1, 17, 0)
libVersion = Version(1, 18, 0)
android(useViewBinding = true)
@ -48,6 +48,7 @@ dependencies {
`appcompat`,
`constraint-layout`,
`fragment`,
`lifecycle-extensions`,
`material`
)

View File

@ -0,0 +1,61 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.presentation.app
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.coroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.onSubscription
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
open class AppLifecycleObserver : AppLifecycleProvider, LifecycleObserver {
private val mutableSharedState = MutableSharedFlow<AppLifecycleProvider.State>(
replay = 1,
onBufferOverflow = BufferOverflow.SUSPEND
)
@OnLifecycleEvent(Lifecycle.Event.ON_START)
open fun onEnterForeground() {
mutableSharedState.tryEmit(AppLifecycleProvider.State.Foreground)
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
open fun onEnterBackground() {
mutableSharedState.tryEmit(AppLifecycleProvider.State.Background)
}
override val lifecycle: Lifecycle by lazy {
ProcessLifecycleOwner.get().lifecycle
}
override val state: StateFlow<AppLifecycleProvider.State> by lazy {
mutableSharedState
.onSubscription { withContext(Dispatchers.Main) { lifecycle.addObserver(this@AppLifecycleObserver) } }
.stateIn(lifecycle.coroutineScope, SharingStarted.Lazily, AppLifecycleProvider.State.Background)
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.presentation.app
import androidx.lifecycle.Lifecycle
import kotlinx.coroutines.flow.StateFlow
interface AppLifecycleProvider {
val lifecycle: Lifecycle
val state: StateFlow<State>
enum class State {
Foreground,
Background
}
}

View File

@ -23,7 +23,7 @@ plugins {
kotlin("android")
}
libVersion = Version(1, 16, 1)
libVersion = Version(1, 18, 0)
android()

View File

@ -40,8 +40,10 @@ dependencies {
project(Module.userData),
project(Module.cryptoCommon),
project(Module.key),
project(Module.eventManagerDomain),
// Other
`javax-inject`,
`retrofit`,
`retrofit-kotlin-serialization`,
`room-ktx`,

View File

@ -0,0 +1,68 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.usersettings.data
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import me.proton.core.domain.entity.UserId
import me.proton.core.eventmanager.domain.EventListener
import me.proton.core.eventmanager.domain.entity.Action
import me.proton.core.eventmanager.domain.entity.Event
import me.proton.core.eventmanager.domain.entity.EventsResponse
import me.proton.core.usersettings.data.api.response.UserSettingsResponse
import me.proton.core.usersettings.data.db.UserSettingsDatabase
import me.proton.core.usersettings.data.extension.toUserSettings
import me.proton.core.usersettings.domain.repository.UserSettingsRepository
import me.proton.core.util.kotlin.deserializeOrNull
import javax.inject.Inject
import javax.inject.Singleton
@Serializable
data class UserSettingsEvents(
@SerialName("UserSettings")
val settings: UserSettingsResponse
)
@Singleton
class UserSettingsEventListener @Inject constructor(
private val db: UserSettingsDatabase,
private val repository: UserSettingsRepository
) : EventListener<String, UserSettingsResponse>() {
override val type = Type.Core
override val order = 1
override suspend fun deserializeEvents(response: EventsResponse): List<Event<String, UserSettingsResponse>>? {
return response.body.deserializeOrNull<UserSettingsEvents>()?.let {
listOf(Event(Action.Update, "null", it.settings))
}
}
override suspend fun <R> inTransaction(block: suspend () -> R): R {
return db.inTransaction(block)
}
override suspend fun onUpdate(userId: UserId, entities: List<UserSettingsResponse>) {
repository.updateUserSettings(entities.first().toUserSettings(userId))
}
override suspend fun onResetAll(userId: UserId) {
repository.getUserSettings(userId, refresh = true)
}
}

View File

@ -20,11 +20,6 @@ package me.proton.core.usersettings.data.api.response
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import me.proton.core.usersettings.domain.entity.Flags
import me.proton.core.usersettings.domain.entity.PasswordSetting
import me.proton.core.usersettings.domain.entity.TwoFASetting
import me.proton.core.usersettings.domain.entity.U2FKeySetting
import me.proton.core.util.kotlin.toBoolean
@Serializable
data class UserSettingsResponse(

View File

@ -23,7 +23,7 @@ plugins {
kotlin("android")
}
libVersion = Version(1, 16, 2)
libVersion = Version(1, 18, 0)
android()

View File

@ -38,6 +38,7 @@ dependencies {
project(Module.dataRoom),
project(Module.domain),
project(Module.userDomain),
project(Module.eventManagerDomain),
project(Module.cryptoCommon),
// Features
@ -70,5 +71,6 @@ dependencies {
project(Module.gopenpgp),
project(Module.userSettings),
project(Module.contact),
project(Module.eventManager),
)
}

View File

@ -68,7 +68,6 @@ import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
class UserAddressRepositoryImplTests {

View File

@ -0,0 +1,88 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.user.data
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import me.proton.core.domain.entity.UserId
import me.proton.core.eventmanager.domain.EventListener
import me.proton.core.eventmanager.domain.entity.Action
import me.proton.core.eventmanager.domain.entity.Event
import me.proton.core.eventmanager.domain.entity.EventsResponse
import me.proton.core.key.data.api.response.AddressResponse
import me.proton.core.user.data.db.AddressDatabase
import me.proton.core.user.data.extension.toAddress
import me.proton.core.user.domain.entity.AddressId
import me.proton.core.user.domain.repository.UserAddressRepository
import me.proton.core.util.kotlin.deserializeOrNull
import javax.inject.Inject
import javax.inject.Singleton
@Serializable
data class UserAddressEvents(
@SerialName("Addresses")
val addresses: List<UserAddressEvent>
)
@Serializable
data class UserAddressEvent(
@SerialName("ID")
val id: String,
@SerialName("Action")
val action: Int,
@SerialName("Address")
val address: AddressResponse
)
@Singleton
class UserAddressEventListener @Inject constructor(
private val db: AddressDatabase,
private val userAddressRepository: UserAddressRepository
) : EventListener<String, AddressResponse>() {
override val type = Type.Core
override val order = 1
override suspend fun deserializeEvents(response: EventsResponse): List<Event<String, AddressResponse>>? {
return response.body.deserializeOrNull<UserAddressEvents>()?.addresses?.map {
Event(requireNotNull(Action.map[it.action]), it.address.id, it.address)
}
}
override suspend fun <R> inTransaction(block: suspend () -> R): R {
return db.inTransaction(block)
}
override suspend fun onCreate(userId: UserId, entities: List<AddressResponse>) {
userAddressRepository.updateAddresses(entities.map { it.toAddress(userId) })
}
override suspend fun onUpdate(userId: UserId, entities: List<AddressResponse>) {
userAddressRepository.updateAddresses(entities.map { it.toAddress(userId) })
}
override suspend fun onDelete(userId: UserId, keys: List<String>) {
userAddressRepository.deleteAddresses(keys.map { AddressId(it) })
}
override suspend fun onResetAll(userId: UserId) {
userAddressRepository.deleteAllAddresses(userId)
userAddressRepository.getAddresses(userId, refresh = true)
}
}

View File

@ -0,0 +1,68 @@
/*
* Copyright (c) 2021 Proton Technologies AG
* This file is part of Proton Technologies AG and ProtonCore.
*
* ProtonCore 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.
*
* ProtonCore 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 ProtonCore. If not, see <https://www.gnu.org/licenses/>.
*/
package me.proton.core.user.data
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import me.proton.core.domain.entity.UserId
import me.proton.core.eventmanager.domain.EventListener
import me.proton.core.eventmanager.domain.entity.Action
import me.proton.core.eventmanager.domain.entity.Event
import me.proton.core.eventmanager.domain.entity.EventsResponse
import me.proton.core.key.data.api.response.UserResponse
import me.proton.core.user.data.db.UserDatabase
import me.proton.core.user.data.extension.toUser
import me.proton.core.user.domain.repository.UserRepository
import me.proton.core.util.kotlin.deserializeOrNull
import javax.inject.Inject
import javax.inject.Singleton
@Serializable
data class UserEvents(
@SerialName("User")
val user: UserResponse
)
@Singleton
class UserEventListener @Inject constructor(
private val db: UserDatabase,
private val userRepository: UserRepository
) : EventListener<String, UserResponse>() {
override val type = Type.Core
override val order = 0
override suspend fun deserializeEvents(response: EventsResponse): List<Event<String, UserResponse>>? {
return response.body.deserializeOrNull<UserEvents>()?.let {
listOf(Event(Action.Update, it.user.id, it.user))
}
}
override suspend fun <R> inTransaction(block: suspend () -> R): R {
return db.inTransaction(block)
}
override suspend fun onUpdate(userId: UserId, entities: List<UserResponse>) {
userRepository.updateUser(entities.first().toUser())
}
override suspend fun onResetAll(userId: UserId) {
userRepository.getUser(userId, refresh = true)
}
}

View File

@ -49,7 +49,6 @@ import me.proton.core.user.domain.entity.UserAddress
import me.proton.core.user.domain.entity.UserAddressKey
import me.proton.core.user.domain.repository.UserAddressRepository
import me.proton.core.user.domain.repository.UserRepository
import me.proton.core.util.kotlin.takeIfNotEmpty
class UserAddressRepositoryImpl(
private val db: AddressDatabase,
@ -120,6 +119,9 @@ class UserAddressRepositoryImpl(
private suspend fun delete(userId: UserId) =
addressDao.deleteAll(userId)
private suspend fun deleteAll(userId: UserId) =
addressDao.deleteAll(userId)
private suspend fun deleteAll() =
addressDao.deleteAll()
@ -138,6 +140,9 @@ class UserAddressRepositoryImpl(
override suspend fun deleteAddresses(addressIds: List<AddressId>) =
delete(*addressIds.toTypedArray())
override suspend fun deleteAllAddresses(userId: UserId) =
deleteAll(userId)
override fun getAddressesFlow(sessionUserId: SessionUserId, refresh: Boolean): Flow<DataResult<List<UserAddress>>> =
store.stream(StoreRequest.cached(StoreKey(sessionUserId), refresh = refresh)).map { it.toDataResult() }

View File

@ -21,6 +21,7 @@ package me.proton.core.user.domain.repository
import kotlinx.coroutines.flow.Flow
import me.proton.core.domain.arch.DataResult
import me.proton.core.domain.entity.SessionUserId
import me.proton.core.domain.entity.UserId
import me.proton.core.user.domain.entity.AddressId
import me.proton.core.user.domain.entity.UserAddress
@ -56,6 +57,15 @@ interface UserAddressRepository {
addressIds: List<AddressId>
)
/**
* Delete all [UserAddress] for a given [userId], locally.
*
* Note: This function is usually used for Events handling.
*/
suspend fun deleteAllAddresses(
userId: UserId
)
/**
* Get all [UserAddress], using [sessionUserId].
*