Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Advanced repeat rules #8456

Open
wants to merge 32 commits into
base: dev-calendar
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
bf3bca9
Prototype event generation using ByRules
andrehgdias Nov 28, 2024
969efeb
Implements event expansion for daily BYDAY and BYMONTH
Dec 2, 2024
996a526
Implements event expansion for Weekly interval
Dec 3, 2024
bd1535b
Implements event expansion for Monthly interval
Dec 3, 2024
c0b3162
Implements expansion for Yearly interval
Dec 9, 2024
8c232d8
Add export of advanced repeat rules
Patrik-wav Dec 10, 2024
08654de
Implements BYMONTHDAY filtering
andrehgdias Dec 12, 2024
3f1af8d
Filter events happening before progenitor
andrehgdias Dec 12, 2024
cdf11ef
Implements BYSETPOS filtering
andrehgdias Dec 9, 2024
35d61db
Fixes BYWEEKNO expansion
andrehgdias Dec 11, 2024
bb0125a
Implements BYYEARDAY filtering
Dec 16, 2024
55c93b6
Fixes SETPOS filtering
Dec 17, 2024
8579a65
Simplifies generator with advanced rules
Dec 17, 2024
27c3fa0
[SDK] Implements BYMONTH expansion
Dec 18, 2024
c5444c2
[SDK] Implements BYWEEKNO expansion
Dec 18, 2024
709052e
[SDK] Implements BYYEARDAY expansion
Dec 19, 2024
195e51d
[SDK] Implements BYMONTHDAY expansion
Dec 19, 2024
f824e43
[SDK] Implements BYDAY expansion
Dec 20, 2024
e49b05f
[SDK] Implements tests for the complete recurrence generation flow
Jan 6, 2025
2eb154a
[SDK] Exposes EventFacade to uniffi
Jan 8, 2025
69a3ab4
[Android] Integrates SDK event expansion during alarm scheduling
Jan 8, 2025
0cddabe
[iOS] Integrates SDK event expansion during alarm scheduling
murilopereirame Jan 13, 2025
0210719
Fixes BYSETPOS on Web/Desktop
Jan 14, 2025
c8afdec
[Desktop] Integrates event expansion during alarm scheduling
Jan 14, 2025
a6ebaf6
[iOS] Adds BYSETPOS handling during alarm schedule
murilopereirame Jan 15, 2025
2478b96
[Android] Fixes BYSETPOS during alarm schedule
Jan 15, 2025
4be71a3
Adds info banner for unsupported rules
Jan 16, 2025
d0ab9dc
Adds translations to Advanced Repeat Rules
murilopereirame Jan 13, 2025
90f2a61
Fixes styling and linting
Feb 3, 2025
279f53e
[Android] Update encryption tests
Feb 5, 2025
1e7f78c
[Android] Adds dexmaker to mock classes during Instrumented Tests
Feb 4, 2025
6e404da
[iOS] Fix iOS tests and SDK event expansion
Feb 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions app-android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ android {
buildTypes.each {
it.buildConfigField 'String', 'FILE_PROVIDER_AUTHORITY', '"' + it.manifestPlaceholders['contentProviderAuthority'] + '"'
// keep in sync with src/native/main/NativePushServiceApp.ts
it.buildConfigField 'String', "SYS_MODEL_VERSION", '"99"'
it.buildConfigField 'String', "TUTANOTA_MODEL_VERSION", '"73"'
it.buildConfigField 'String', "SYS_MODEL_VERSION", '"119"'
it.buildConfigField 'String', "TUTANOTA_MODEL_VERSION", '"80"'
it.buildConfigField 'String', 'RES_ADDRESS', '"tutanota"'
}

Expand Down Expand Up @@ -174,11 +174,15 @@ dependencies {
// JVM-based unit tests (that don't need a real device or emulator)
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"

androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito-inline-extended:2.28.1") {
exclude group: 'org.mockito', module: 'mockito-core'
}
androidTestImplementation "org.mockito:mockito-core:5.15.2"
androidTestImplementation "org.mockito.kotlin:mockito-kotlin:5.4.0"
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
androidTestImplementation 'androidx.test:runner:1.6.1'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.2.1'
androidTestImplementation 'androidx.test:rules:1.6.1'
androidTestImplementation 'org.mockito:mockito-android:5.12.0'
androidTestImplementation 'com.fasterxml.jackson.core:jackson-databind:2.17.2'
androidTestImplementation 'androidx.room:room-testing:2.6.1'
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class AlarmModelTest {
val eventStart = getDate(timeZone, 2019, 4, 2, 12, 0)
iterateAlarmOccurrences(
now, timeZone, eventStart, eventStart, RepeatPeriod.WEEKLY,
1, EndType.NEVER, 0, AlarmInterval(AlarmIntervalUnit.HOUR, 1), timeZone, emptyList()
1, EndType.NEVER, 0, AlarmInterval(AlarmIntervalUnit.HOUR, 1), timeZone, emptyList(), emptyList()
) { time: Date, _: Int, _: Date? -> occurrences.add(time) }
Assert.assertArrayEquals(
listOf(
Expand All @@ -45,8 +45,18 @@ class AlarmModelTest {
val eventEnd = getAllDayDateUTC(getDate(timeZone, 2019, 4, 3, 0, 0), timeZone)
val repeatEnd = getAllDayDateUTC(getDate(timeZone, 2019, 4, 4, 0, 0), timeZone)
iterateAlarmOccurrences(
now, repeatTimeZone, eventStart, eventEnd, RepeatPeriod.DAILY,
1, EndType.UNTIL, repeatEnd.time, AlarmInterval(AlarmIntervalUnit.DAY, 1), timeZone, emptyList()
now,
repeatTimeZone,
eventStart,
eventEnd,
RepeatPeriod.DAILY,
1,
EndType.UNTIL,
repeatEnd.time,
AlarmInterval(AlarmIntervalUnit.DAY, 1),
timeZone,
emptyList(),
emptyList()
) { time: Date, _: Int, _: Date? -> occurrences.add(time) }
val expected = listOf( // Event on 2nd, alarm on 1st
getDate(timeZone, 2019, 4, 1, 0, 0), // Event on 3rd, alarm on 2d
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,20 @@ import de.tutao.tutashared.push.SseStorage
import de.tutao.tutashared.toBase64
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mockito
import org.mockito.invocation.InvocationOnMock
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
import org.mockito.stubbing.Answer
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.ConscryptMode
import java.security.KeyStoreException
import java.security.UnrecoverableEntryException
import java.util.Calendar
import java.util.Date
import java.util.concurrent.TimeUnit

@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
@ConscryptMode(ConscryptMode.Mode.OFF)
class AlarmNotificationsManagerTest {

class AlarmNotificationsManagerTest {
private lateinit var manager: AlarmNotificationsManager

private lateinit var systemAlarmFacade: SystemAlarmFacade
Expand Down Expand Up @@ -77,7 +70,15 @@ class AlarmNotificationsManagerTest {
val repeatingAlarmIdentifier = "repeatingAlarmIdentifier"
val alarmNotification = createEncryptedAlarmNotification(userId, singleAlarmIdentifier, null, null)
val repeatRule =
EncryptedRepeatRule("1", "1", "Europe/Berlin", EndType.COUNT.ordinal.toString(), "2", emptyList())
EncryptedRepeatRule(
"1",
"1",
"Europe/Berlin",
EndType.COUNT.ordinal.toString(),
"2",
emptyList(),
emptyList()
)
val repeatingAlarmNotification = createEncryptedAlarmNotification(
userId, repeatingAlarmIdentifier, null, repeatRule
)
Expand Down Expand Up @@ -126,7 +127,15 @@ class AlarmNotificationsManagerTest {
val identifier = "notTooFarR"
val startDate = Date(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1))
val repeatRule =
EncryptedRepeatRule(RepeatPeriod.WEEKLY.value().toString(), "1", "Europe/Berlin", "0", "0", emptyList())
EncryptedRepeatRule(
RepeatPeriod.WEEKLY.value().toString(),
"1",
"Europe/Berlin",
"0",
"0",
emptyList(),
emptyList()
)
val alarmNotification = createEncryptedAlarmNotification(userId, identifier, startDate, repeatRule)
manager.scheduleNewAlarms(listOf(alarmNotification))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package de.tutao.tutanota.alarms
import android.util.Log
import de.tutao.tutanota.*
import de.tutao.tutanota.push.LocalNotificationsFacade
import de.tutao.tutasdk.ByRule
import de.tutao.tutashared.AndroidNativeCryptoFacade
import de.tutao.tutashared.CryptoError
import de.tutao.tutashared.OperationType
Expand Down Expand Up @@ -185,7 +186,6 @@ class AlarmNotificationsManager(
alarmNotification: EncryptedAlarmNotification,
pushKeyResolver: PushKeyResolver,
) {

// The DELETE notification we receive from the server has only placeholder fields and no keys. We must use our saved alarm to cancel notifications.
val savedAlarmNotification = sseStorage.readAlarmNotifications().find {
it.alarmInfo.identifier == alarmNotification.alarmInfo.identifier
Expand Down Expand Up @@ -243,10 +243,12 @@ class AlarmNotificationsManager(
val endValue = repeatRule.endValue
val excludedDates = repeatRule.excludedDates
val alarmTrigger: AlarmInterval = alarmNotification.alarmInfo.trigger
val byRules: List<ByRule> = alarmNotification.repeatRule?.advancedRules ?: listOf()

AlarmModel.iterateAlarmOccurrences(
Date(),
timeZone, eventStart, eventEnd, frequency, interval, endType,
endValue, alarmTrigger, TimeZone.getDefault(), excludedDates, callback
endValue, alarmTrigger, TimeZone.getDefault(), excludedDates, byRules, callback
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import android.util.Log
import de.tutao.tutanota.BuildConfig
import java.util.Date


class SystemAlarmFacade(private val context: Context) {
fun scheduleAlarmOccurrenceWithSystem(
alarmTime: Date,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ private const val ALARM_NOTIFICATION_CHANNEL_ID = "alarms"
private const val DOWNLOAD_NOTIFICATION_CHANNEL_ID = "downloads"
private const val EMAIL_ADDRESS_EXTRA = "email_address"


class LocalNotificationsFacade(private val context: Context, private val sseStorage: SseStorage) {
companion object {
private const val TAG = "LocalNotifications"
Expand Down
16 changes: 10 additions & 6 deletions app-android/calendar/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import com.android.build.gradle.internal.tasks.FinalizeBundleTask
import org.jetbrains.kotlin.util.capitalizeDecapitalize.capitalizeAsciiOnly
import org.gradle.configurationcache.extensions.capitalized

plugins {
id("com.android.application")
Expand Down Expand Up @@ -98,10 +98,10 @@ android {
val taskName = StringBuilder("sign").run {
//Add a task to rename the output file
productFlavors.forEach {
append(it.name.capitalizeAsciiOnly())
append(it.name.capitalized())
}

append(buildType.name.capitalizeAsciiOnly())
append(buildType.name.capitalized())
append("Bundle")

toString()
Expand All @@ -123,8 +123,8 @@ android {
"\"" + it.manifestPlaceholders["contentProviderAuthority"] + "\""
)
// keep in sync with src/native/main/NativePushServiceApp.ts
it.buildConfigField("String", "SYS_MODEL_VERSION", "\"99\"")
it.buildConfigField("String", "TUTANOTA_MODEL_VERSION", "\"73\"")
it.buildConfigField("String", "SYS_MODEL_VERSION", "\"119\"")
it.buildConfigField("String", "TUTANOTA_MODEL_VERSION", "\"80\"")
it.buildConfigField("String", "RES_ADDRESS", "\"tutanota\"")
}

Expand Down Expand Up @@ -208,11 +208,15 @@ dependencies {
// JVM-based unit tests (that don't need a real device or emulator)
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version")

androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito-inline-extended:2.28.1") {
exclude(group = "org.mockito", module = "mockito-core")
}
androidTestImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
androidTestImplementation("org.mockito:mockito-core:5.15.2")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
androidTestImplementation("androidx.test:runner:1.4.0")
androidTestImplementation("androidx.test.ext:junit-ktx:1.1.3")
androidTestImplementation("androidx.test:rules:1.4.0")
androidTestImplementation("org.mockito:mockito-android:5.11.0")
androidTestImplementation("com.fasterxml.jackson.core:jackson-databind:2.15.2")
androidTestImplementation("androidx.room:room-testing:2.4.2")
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class AlarmModelTest {
val eventStart = getDate(timeZone, 2019, 4, 2, 12, 0)
iterateAlarmOccurrences(
now, timeZone, eventStart, eventStart, RepeatPeriod.WEEKLY,
1, EndType.NEVER, 0, AlarmInterval(AlarmIntervalUnit.HOUR, 1), timeZone, emptyList()
1, EndType.NEVER, 0, AlarmInterval(AlarmIntervalUnit.HOUR, 1), timeZone, emptyList(), emptyList()
) { time: Date, _: Int, _: Date? -> occurrences.add(time) }
Assert.assertArrayEquals(
listOf(
Expand All @@ -45,8 +45,18 @@ class AlarmModelTest {
val eventEnd = getAllDayDateUTC(getDate(timeZone, 2019, 4, 3, 0, 0), timeZone)
val repeatEnd = getAllDayDateUTC(getDate(timeZone, 2019, 4, 4, 0, 0), timeZone)
iterateAlarmOccurrences(
now, repeatTimeZone, eventStart, eventEnd, RepeatPeriod.DAILY,
1, EndType.UNTIL, repeatEnd.time, AlarmInterval(AlarmIntervalUnit.DAY, 1), timeZone, emptyList()
now,
repeatTimeZone,
eventStart,
eventEnd,
RepeatPeriod.DAILY,
1,
EndType.UNTIL,
repeatEnd.time,
AlarmInterval(AlarmIntervalUnit.DAY, 1),
timeZone,
emptyList(),
emptyList()
) { time: Date, _: Int, _: Date? -> occurrences.add(time) }
val expected = listOf( // Event on 2nd, alarm on 1st
getDate(timeZone, 2019, 4, 1, 0, 0), // Event on 3rd, alarm on 2d
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,18 @@ import de.tutao.tutashared.push.SseStorage
import de.tutao.tutashared.toBase64
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mockito
import org.mockito.invocation.InvocationOnMock
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
import org.mockito.stubbing.Answer
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.ConscryptMode
import java.security.KeyStoreException
import java.security.UnrecoverableEntryException
import java.util.Calendar
import java.util.Date
import java.util.concurrent.TimeUnit

@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
@ConscryptMode(ConscryptMode.Mode.OFF)
class AlarmNotificationsManagerTest {

private lateinit var manager: AlarmNotificationsManager
Expand Down Expand Up @@ -76,7 +69,15 @@ class AlarmNotificationsManagerTest {
val repeatingAlarmIdentifier = "repeatingAlarmIdentifier"
val alarmNotification = createEncryptedAlarmNotification(userId, singleAlarmIdentifier, null, null)
val repeatRule =
EncryptedRepeatRule("1", "1", "Europe/Berlin", EndType.COUNT.ordinal.toString(), "2", emptyList())
EncryptedRepeatRule(
"1",
"1",
"Europe/Berlin",
EndType.COUNT.ordinal.toString(),
"2",
emptyList(),
emptyList()
)
val repeatingAlarmNotification = createEncryptedAlarmNotification(
userId, repeatingAlarmIdentifier, null, repeatRule
)
Expand Down Expand Up @@ -125,7 +126,15 @@ class AlarmNotificationsManagerTest {
val identifier = "notTooFarR"
val startDate = Date(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1))
val repeatRule =
EncryptedRepeatRule(RepeatPeriod.WEEKLY.value().toString(), "1", "Europe/Berlin", "0", "0", emptyList())
EncryptedRepeatRule(
RepeatPeriod.WEEKLY.value().toString(),
"1",
"Europe/Berlin",
"0",
"0",
emptyList(),
emptyList()
)
val alarmNotification = createEncryptedAlarmNotification(userId, identifier, startDate, repeatRule)
manager.scheduleNewAlarms(listOf(alarmNotification))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,18 +193,12 @@ class CompatibilityTest {
}

@Test
suspend fun argon2idTest() {
fun argon2idTest() = runBlocking {
for (td in testData.argon2idTests) {
val key = hexToBytes(td.keyHex)
val salt = hexToBytes(td.saltHex)
val result = crypto.argon2idGeneratePassphraseKey(
td.password,
salt.wrap()
)
assertArrayEquals(
key,
result.data
)
val password = td.password
assertArrayEquals(key, crypto.argon2idGeneratePassphraseKey(password, salt.wrap()).data)
}
}

Expand All @@ -218,9 +212,8 @@ class CompatibilityTest {
hexToBytes(testData.input).wrap(),
hexToBytes(testData.seed).wrap()
).data
//String hexResult = bytesToHex(encryptedResultBytes);
//assertEquals(testData.getResult(), hexResult);
//cannot compare encrypted test data because default android implementation ignores randomizer
val hexResult = bytesToHex(encryptedResult)
assertEquals(testData.result, hexResult)
val plainText = crypto.rsaDecrypt(hexToPrivateKey(testData.privateKey), encryptedResult.wrap()).data
assertEquals(testData.input, bytesToHex(plainText))
val plainTextFromTestData =
Expand All @@ -230,15 +223,23 @@ class CompatibilityTest {
}

@Test
@Throws(CryptoError::class)
fun kyber() = runBlocking {
fun kyber_roundtrip() = runBlocking {
for (td in testData.kyberEncryptionTests) {
// we need to use the same seed so that we always obtain the same encapsulation
val privateKey: KyberPrivateKey = hexToKyberPrivateKey(td.privateKey)
val publicKey: KyberPublicKey = hexToKyberPublicKey(td.publicKey)
val encapsulation = crypto.kyberEncapsulate(publicKey, hexToBytes(td.seed).wrap())
assertEquals(td.cipherText, bytesToHex(encapsulation.ciphertext.data))
assertEquals(td.sharedSecret, bytesToHex(encapsulation.sharedSecret.data))
val sharedSecret = crypto.kyberDecapsulate(privateKey, encapsulation.ciphertext)
assertEquals(encapsulation.sharedSecret, sharedSecret)
}
}

@Test
@Throws(CryptoError::class)
fun kyber() = runBlocking {
for (td in testData.kyberEncryptionTests) {
// we can't test encapsulation because we can't inject entropy in our current impl, only test decapsulation
// and roundtrip in another test
val privateKey: KyberPrivateKey = hexToKyberPrivateKey(td.privateKey)
val sharedSecret = crypto.kyberDecapsulate(privateKey, hexToBytes(td.cipherText).wrap())
assertEquals(td.sharedSecret, bytesToHex(sharedSecret.data))
}
Expand Down Expand Up @@ -381,9 +382,5 @@ class CompatibilityTest {
}
}
}

init {
System.loadLibrary("tutanota")
}
}
}
Loading