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

Merged
merged 33 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
811ac1b
Prototype event generation using ByRules
andrehgdias Nov 28, 2024
2b6983c
Implements event expansion for daily BYDAY and BYMONTH
Dec 2, 2024
9b10f95
Implements event expansion for Weekly interval
Dec 3, 2024
7b1b3f3
Implements event expansion for Monthly interval
Dec 3, 2024
971d6c8
Implements expansion for Yearly interval
Dec 9, 2024
9f7e6bd
Add export of advanced repeat rules
Patrik-wav Dec 10, 2024
60ff001
Implements BYMONTHDAY filtering
andrehgdias Dec 12, 2024
cac89f0
Filter events happening before progenitor
andrehgdias Dec 12, 2024
3827ed9
Implements BYSETPOS filtering
andrehgdias Dec 9, 2024
d7e0e8b
Fixes BYWEEKNO expansion
andrehgdias Dec 11, 2024
38c799a
Implements BYYEARDAY filtering
Dec 16, 2024
1aacef8
Fixes SETPOS filtering
Dec 17, 2024
7ce09c1
Simplifies generator with advanced rules
Dec 17, 2024
b1121c8
[SDK] Implements BYMONTH expansion
Dec 18, 2024
f2a6781
[SDK] Implements BYWEEKNO expansion
Dec 18, 2024
c0956a4
[SDK] Implements BYYEARDAY expansion
Dec 19, 2024
5af07b1
[SDK] Implements BYMONTHDAY expansion
Dec 19, 2024
4d3d066
[SDK] Implements BYDAY expansion
Dec 20, 2024
ea406c6
[SDK] Implements tests for the complete recurrence generation flow
Jan 6, 2025
d61281c
[SDK] Exposes EventFacade to uniffi
Jan 8, 2025
c8dfd98
[Android] Integrates SDK event expansion during alarm scheduling
Jan 8, 2025
a06961a
[iOS] Integrates SDK event expansion during alarm scheduling
murilopereirame Jan 13, 2025
7ce738c
Fixes BYSETPOS on Web/Desktop
Jan 14, 2025
fead3ed
[Desktop] Integrates event expansion during alarm scheduling
Jan 14, 2025
0e0c0e1
[iOS] Adds BYSETPOS handling during alarm schedule
murilopereirame Jan 15, 2025
bfc06b0
[Android] Fixes BYSETPOS during alarm schedule
Jan 15, 2025
52662bf
Adds info banner for unsupported rules
Jan 16, 2025
4785dd7
Adds translations to Advanced Repeat Rules
murilopereirame Jan 13, 2025
dfa4faa
Fixes styling and linting
Feb 3, 2025
1cff871
[Android] Update encryption tests
Feb 5, 2025
5d5d261
[Android] Adds dexmaker to mock classes during Instrumented Tests
Feb 4, 2025
3901d4c
[iOS] Fix iOS tests and SDK event expansion
Feb 6, 2025
945ddb4
Changes after review
murilopereirame Feb 11, 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
3 changes: 3 additions & 0 deletions Cargo.lock

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

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
Loading
Loading