Explorar o código

Version 6.1.0-1082

Threema hai 4 meses
pai
achega
18faf807c3
Modificáronse 100 ficheiros con 793 adicións e 1350 borrados
  1. 4 0
      .editorconfig
  2. 1 1
      .github/pull_request_template.md
  3. 2 2
      CONTRIBUTING.md
  4. 12 9
      README.md
  5. 2 2
      SECURITY.md
  6. 29 19
      app/assets/license.html
  7. 52 10
      app/build.gradle.kts
  8. 11 17
      app/src/androidTest/java/ch/threema/app/TestCoreServiceManager.kt
  9. 1 1
      app/src/androidTest/java/ch/threema/app/backuprestore/csv/BackupServiceTest.java
  10. 2 1
      app/src/androidTest/java/ch/threema/app/contacts/AddOrUpdateContactBackgroundTaskTest.kt
  11. 14 19
      app/src/androidTest/java/ch/threema/app/contacts/MarkContactAsDeletedBackgroundTaskTest.kt
  12. 4 3
      app/src/androidTest/java/ch/threema/app/contacts/ReflectedContactSyncTaskTest.kt
  13. 6 7
      app/src/androidTest/java/ch/threema/app/edithistory/EditHistoryTest.kt
  14. 4 4
      app/src/androidTest/java/ch/threema/app/emojis/MarkupParserTest.java
  15. 47 22
      app/src/androidTest/java/ch/threema/app/groupmanagement/CreateGroupFlowTest.kt
  16. 33 15
      app/src/androidTest/java/ch/threema/app/groupmanagement/DisbandGroupFlowTest.kt
  17. 1 1
      app/src/androidTest/java/ch/threema/app/groupmanagement/GroupControlTest.kt
  18. 2 2
      app/src/androidTest/java/ch/threema/app/groupmanagement/GroupConversationListTest.kt
  19. 44 1
      app/src/androidTest/java/ch/threema/app/groupmanagement/GroupFlowTest.kt
  20. 16 14
      app/src/androidTest/java/ch/threema/app/groupmanagement/GroupResyncFlowTest.kt
  21. 4 4
      app/src/androidTest/java/ch/threema/app/groupmanagement/IncomingGroupLeaveTest.kt
  22. 3 3
      app/src/androidTest/java/ch/threema/app/groupmanagement/IncomingGroupNameTest.kt
  23. 5 5
      app/src/androidTest/java/ch/threema/app/groupmanagement/IncomingGroupSetupTest.kt
  24. 1 1
      app/src/androidTest/java/ch/threema/app/groupmanagement/IncomingGroupSyncRequestTest.kt
  25. 1 1
      app/src/androidTest/java/ch/threema/app/groupmanagement/IncomingGroupTextTest.kt
  26. 33 16
      app/src/androidTest/java/ch/threema/app/groupmanagement/LeaveGroupFlowTest.kt
  27. 32 18
      app/src/androidTest/java/ch/threema/app/groupmanagement/RemoveGroupFlowTest.kt
  28. 64 52
      app/src/androidTest/java/ch/threema/app/groupmanagement/UpdateGroupFlowTest.kt
  29. 14 14
      app/src/androidTest/java/ch/threema/app/processors/IncomingMessageProcessorTest.kt
  30. 14 13
      app/src/androidTest/java/ch/threema/app/processors/MessageProcessorProvider.kt
  31. 10 9
      app/src/androidTest/java/ch/threema/app/protocol/IdentityBlockedStepsTest.kt
  32. 0 340
      app/src/androidTest/java/ch/threema/app/service/GroupInviteServiceTest.java
  33. 99 0
      app/src/androidTest/java/ch/threema/app/service/GroupInviteServiceTest.kt
  34. 11 9
      app/src/androidTest/java/ch/threema/app/services/BlockedIdentitiesServiceTest.kt
  35. 2 2
      app/src/androidTest/java/ch/threema/app/tasks/GroupCreateTaskTest.kt
  36. 73 1
      app/src/androidTest/java/ch/threema/app/tasks/PersistableTasksTest.kt
  37. 4 4
      app/src/androidTest/java/ch/threema/app/testutils/AndroidTestUtils.kt
  38. 0 71
      app/src/androidTest/java/ch/threema/app/testutils/CaptureLogcatOnTestFailureRule.java
  39. 0 55
      app/src/androidTest/java/ch/threema/app/testutils/InstructionUtil.java
  40. 1 63
      app/src/androidTest/java/ch/threema/app/testutils/TestHelpers.java
  41. 1 1
      app/src/androidTest/java/ch/threema/app/utils/BackgroundExecutorTest.kt
  42. 7 7
      app/src/androidTest/java/ch/threema/app/utils/BundledMessagesSendStepsTest.kt
  43. 0 60
      app/src/androidTest/java/ch/threema/app/utils/GeoLocationUtilTest.kt
  44. 8 8
      app/src/androidTest/java/ch/threema/app/utils/LinkifyUtilTest.kt
  45. 9 8
      app/src/androidTest/java/ch/threema/app/voip/SdpTest.java
  46. 4 4
      app/src/androidTest/java/ch/threema/app/webclient/activities/SessionsActivityTest.java
  47. 5 7
      app/src/androidTest/java/ch/threema/data/TestDatabaseService.kt
  48. 8 7
      app/src/androidTest/java/ch/threema/data/repositories/ContactModelRepositoryTest.kt
  49. 5 4
      app/src/androidTest/java/ch/threema/data/repositories/EditHistoryRepositoryTest.kt
  50. 5 4
      app/src/androidTest/java/ch/threema/data/repositories/EmojiReactionsRepositoryTest.kt
  51. 5 4
      app/src/androidTest/java/ch/threema/data/repositories/GroupModelRepositoryTest.kt
  52. 20 18
      app/src/androidTest/java/ch/threema/storage/DatabaseExtensionsTest.kt
  53. 5 5
      app/src/androidTest/java/ch/threema/storage/DatabaseNonceStoreTest.kt
  54. 1 3
      app/src/androidTest/java/ch/threema/storage/SQLDHSessionStoreTest.java
  55. 6 6
      app/src/androidTest/java/ch/threema/storage/TaskArchiveFactoryTest.kt
  56. 0 72
      app/src/androidTest/java/com/azimolabs/conditionwatcher/ConditionWatcher.java
  57. 0 35
      app/src/androidTest/java/com/azimolabs/conditionwatcher/Instruction.java
  58. BIN=BIN
      app/src/blue/ic_launcher-playstore.png
  59. BIN=BIN
      app/src/blue/ic_launcher-web.png
  60. 17 11
      app/src/blue/java/ch/threema/app/compose/theme/color/BrandColor.kt
  61. 0 60
      app/src/blue/java/ch/threema/app/compose/theme/color/ColorsDark.kt
  62. 0 60
      app/src/blue/java/ch/threema/app/compose/theme/color/ColorsLight.kt
  63. BIN=BIN
      app/src/blue/res/drawable-hdpi/ic_notification_multi.png
  64. BIN=BIN
      app/src/blue/res/drawable-hdpi/ic_notification_small.png
  65. BIN=BIN
      app/src/blue/res/drawable-hdpi/logo_main_white.png
  66. BIN=BIN
      app/src/blue/res/drawable-mdpi/ic_notification_multi.png
  67. BIN=BIN
      app/src/blue/res/drawable-mdpi/ic_notification_small.png
  68. BIN=BIN
      app/src/blue/res/drawable-mdpi/logo_main_white.png
  69. 0 116
      app/src/blue/res/drawable-v24/ic_launcher_foreground.xml
  70. 0 19
      app/src/blue/res/drawable-v24/ic_launcher_monochrome.xml
  71. 29 0
      app/src/blue/res/drawable-v26/ic_launcher_monochrome.xml
  72. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_01_40ms.png
  73. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_02_40ms.png
  74. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_03_40ms.png
  75. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_04_40ms.png
  76. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_05_40ms.png
  77. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_06_40ms.png
  78. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_07_40ms.png
  79. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_08_40ms.png
  80. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_09_40ms.png
  81. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_10_40ms.png
  82. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_11_40ms.png
  83. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_12_40ms.png
  84. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_13_40ms.png
  85. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_14_40ms.png
  86. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_15_40ms.png
  87. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_16_40ms.png
  88. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_17_40ms.png
  89. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_18_40ms.png
  90. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_19_40ms.png
  91. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_20_40ms.png
  92. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_21_40ms.png
  93. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_22_40ms.png
  94. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_23_40ms.png
  95. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_24_40ms.png
  96. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_25_40ms.png
  97. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_26_40ms.png
  98. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_27_40ms.png
  99. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_28_40ms.png
  100. BIN=BIN
      app/src/blue/res/drawable-xhdpi/anim_29_40ms.png

+ 4 - 0
.editorconfig

@@ -20,3 +20,7 @@ ktlint_function_naming_ignore_when_annotated_with = Composable
 # and do not match our preferences
 ktlint_standard_function-signature = disabled
 ktlint_standard_no-wildcard-imports = disabled
+
+[*.{yml,yaml}]
+indent_size = 2
+ij_any_spaces_within_brackets = false

+ 1 - 1
.github/pull_request_template.md

@@ -8,6 +8,6 @@ please see CONTRIBUTING.md or README.md -->
 
 <!-- Please check the items that apply. -->
 
-- [ ] I signed the [Contributor License Agreement](https://threema.ch/en/open-source/cla)
+- [ ] I signed the [Contributor License Agreement](https://threema.com/en/open-source/cla)
 - [ ] All changes in this pull request were made by me, I own the full copyright
       for all these changes

+ 2 - 2
CONTRIBUTING.md

@@ -3,13 +3,13 @@
 ## Submitting Patches
 
 We accept GitHub pull requests. Please refer to
-<https://threema.ch/open-source/contributions>
+<https://threema.com/open-source/contributions>
 for more information on how to contribute.
 
 ## Bug Reports / Feature Requests
 
 To report bugs and request new features, please contact the Threema support
-team through [threema.ch/support](https://threema.ch/support).
+team through [threema.com/support](https://threema.com/support).
 
 ## Translations
 

+ 12 - 9
README.md

@@ -1,13 +1,13 @@
 <div align="center">
   <!-- Centered README header hack -->
-  <img width="400" src="logo.svg">
+  <img width="500" src="logo.svg">
   <br><br>
 </div>
 
 # Threema for Android
 
 This repository contains the complete source code of
-[Threema](https://threema.ch/) for Android.
+[Threema](https://threema.com/) for Android.
 
 
 ## Table of Contents
@@ -28,14 +28,14 @@ This repository contains the complete source code of
 ## <a name="issues"></a>Bug Reports / Feature Requests / Security Issues
 
 To report bugs and request new features, please contact the Threema support
-team through [threema.ch/support](https://threema.ch/support).
+team through [threema.com/support](https://threema.com/support).
 
 If you discover a security issue in Threema, please adhere to the coordinated
 vulnerability disclosure model. To be eligible for a bug bounty, please [file a
 report on GObugfree](https://app.gobugfree.com/programs/threema) (where all the
 details, including the bounty levels, are listed). If you’re not interested in
 the bug bounty program, you can contact us via Threema or by email; for contact
-details, see [threema.ch/contact](https://threema.ch/en/contact) (section
+details, see [threema.com/contact](https://threema.com/en/contact) (section
 “Security”).
 
 
@@ -106,9 +106,9 @@ Threema employee.
 
 ## <a name="build-variants"></a>Build Variants
 
-**Consumer**
+**Private**
 
-There are currently the following product flavors relevant for the consumer
+There are currently the following product flavors relevant for the private
 version of Threema:
 
 | Flavor                 | Description                                    | License Checks |
@@ -225,13 +225,13 @@ You can also run tests through Android Studio.
 
 Instructions on how to reproduce the build process used to publish the official
 Threema app can be found at
-[threema.ch/open-source/reproducible-builds/](https://threema.ch/open-source/reproducible-builds/).
+[threema.com/open-source/reproducible-builds/](https://threema.com/open-source/reproducible-builds/).
 
 
 ## <a name="architecture"></a>Code Organization / Architecture
 
 Before digging into the codebase, you should read the [Cryptography
-Whitepaper](https://threema.ch/press-files/2_documentation/cryptography_whitepaper.pdf)
+Whitepaper](https://threema.com/press-files/2_documentation/cryptography_whitepaper.pdf)
 to understand the design concepts.
 
 Code related to the core functionality (e.g., connecting to the chat server,
@@ -241,11 +241,14 @@ encrypting messages, etc.) can be found in the
 The code of the actual Android app is located in the
 `app/src/main/java/ch/threema/` directory.
 
+Common code not specific to Threema is located in the
+`common/src/main/java/ch/threema/` directory.
+
 
 ## <a name="contributions"></a>Contributions
 
 We accept GitHub pull requests. Please refer to
-<https://threema.ch/open-source/contributions>
+<https://threema.com/open-source/contributions>
 for more information on how to contribute.
 
 Note that translation fixes should not be contributed through GitHub but

+ 2 - 2
SECURITY.md

@@ -3,7 +3,7 @@
 ## Cryptography Whitepaper
 
 The algorithms and design decisions behind the cryptography are documented in
-the [Cryptography Whitepaper](https://threema.ch/press-files/2_documentation/cryptography_whitepaper.pdf).
+the [Cryptography Whitepaper](https://threema.com/press-files/2_documentation/cryptography_whitepaper.pdf).
 
 ## Reporting Vulnerabilities
 
@@ -12,7 +12,7 @@ vulnerability disclosure model. To be eligible for a bug bounty, please [file a
 report on GObugfree](https://app.gobugfree.com/programs/threema) (where all the
 details, including the bounty levels, are listed). If you’re not interested in
 the bug bounty program, you can contact us via Threema or by email; for contact
-details, see [threema.ch/contact](https://threema.ch/en/contact) (section
+details, see [threema.com/contact](https://threema.com/en/contact) (section
 “Security”).
 
 ## Code Signing

+ 29 - 19
app/assets/license.html

@@ -8,35 +8,36 @@
     <title>Legal notes</title>
     <style type="text/css">
         body {
-        font-family: Helvetica, Arial, sans-serif;
-        font-size: 11px;
-        margin: 0 0;
-        padding: 8px 8px;
-        line-height: 1.3em;
+            font-family: Helvetica, Arial, sans-serif;
+            font-size: 11px;
+            margin: 0 0;
+            padding: 8px 8px;
+            line-height: 1.3em;
         }
 
         h1 {
-        font-size: 16px;
-        line-height: 1.3em;
-        margin-bottom: 1em;
-        margin-top: 0;
+            font-size: 14px;
+            color: #555;
+            border-top: 1px solid #aaa;
+            padding-top: 0.5em;
+            margin-top: 1.5em;
         }
 
         h2 {
-        font-size: 13px;
-        color: #777;
-        border-top: 1px solid #aaa;
-        padding-top: 0.5em;
-        margin-top: 1.5em;
+            font-size: 13px;
+            color: #777;
+            border-top: 1px solid #aaa;
+            padding-top: 0.5em;
+            margin-top: 1.5em;
         }
 
         a {
-        color: #0086C9;
+            color: #0086C9;
         }
 
         .maincopyright {
-        font-size: 14px;
-        line-height: 1.3em;
+            font-size: 14px;
+            line-height: 1.3em;
         }
     </style>
 </head>
@@ -46,8 +47,16 @@
 <p class="maincopyright">Copyright © 2013-2025 Threema GmbH.<br/>
     All rights reserved.</p>
 
-<p>This product contains artwork and code from the following rights holders:</p>
+<h1>Translations</h1>
+
+<p>The app localizations were realized with kind support from various translators.<br />
+    Thank you!</p>
+
+<p>If you would like to help translate the app, please contact us at <a href="mailto:support@threema.ch">support@threema.ch</a>.</p>
 
+<h1>Software</h1>
+
+<p>This product contains artwork and code from the following rights holders:</p>
 
 <h2>Android Fast Scroll</h2>
 
@@ -357,7 +366,7 @@ SUCH DAMAGE.</p>
 
 <h2>SQLCipher</h2>
 
-<p>Copyright (c) 2016 Zetetic LLC<br />
+<p>Copyright (c) 2025 Zetetic LLC<br />
 All rights reserved.</p>
 
 <p>Redistribution and use in source and binary forms, with or without
@@ -668,3 +677,4 @@ POSSIBILITY OF SUCH DAMAGE.</p>
 </body>
 
 </html>
+>

+ 52 - 10
app/build.gradle.kts

@@ -1,3 +1,24 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2014-2025 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
 import com.android.build.gradle.internal.api.ApkVariantOutputImpl
 import config.PublicKeys
 import config.setProductNames
@@ -26,7 +47,7 @@ if (gradle.startParameter.taskRequests.toString().contains("Hms")) {
 /**
  * Only use the scheme "<major>.<minor>.<patch>" for the appVersion
  */
-val appVersion = "6.0.1"
+val appVersion = "6.1.0"
 
 /**
  * betaSuffix with leading dash (e.g. `-beta1`).
@@ -35,7 +56,7 @@ val appVersion = "6.0.1"
  */
 val betaSuffix = ""
 
-val defaultVersionCode = 1074
+val defaultVersionCode = 1082
 
 /**
  * Map with keystore paths (if found).
@@ -58,7 +79,7 @@ android {
     defaultConfig {
         // https://developer.android.com/training/testing/espresso/setup#analytics
         with(testInstrumentationRunnerArguments) {
-            put("notAnnotation", "ch.threema.app.TestFastlaneOnly,ch.threema.app.DangerousTest")
+            put("notAnnotation", "ch.threema.app.DangerousTest")
             put("disableAnalytics", "true")
         }
         minSdk = 21
@@ -111,8 +132,8 @@ android {
         stringBuildConfigField("AVATAR_FETCH_URL", "https://avatar.threema.ch/")
         stringBuildConfigField("SAFE_SERVER_URL", "https://safe-{backupIdPrefix8}.threema.ch/")
         stringBuildConfigField("WEB_SERVER_URL", "https://web.threema.ch/")
-        stringBuildConfigField("APP_RATING_URL", "https://threema.ch/app-rating/android/{rating}")
-        stringBuildConfigField("MAP_STYLES_URL", "https://map.threema.ch/styles/streets/style.json")
+        stringBuildConfigField("APP_RATING_URL", "https://threema.com/app-rating/android/{rating}")
+        stringBuildConfigField("MAP_STYLES_URL", "https://map.threema.ch/styles/threema/style.json")
         stringBuildConfigField("MAP_POI_AROUND_URL", "https://poi.threema.ch/around/{latitude}/{longitude}/{radius}/")
         stringBuildConfigField("MAP_POI_NAMES_URL", "https://poi.threema.ch/names/{latitude}/{longitude}/{query}/")
         byteArrayBuildConfigField("THREEMA_PUSH_PUBLIC_KEY", PublicKeys.threemaPush)
@@ -124,6 +145,7 @@ android {
         booleanBuildConfigField("MD_SYNC_DISTRIBUTION_LISTS", false)
         booleanBuildConfigField("EDIT_MESSAGES_ENABLED", true)
         booleanBuildConfigField("DELETE_MESSAGES_ENABLED", true)
+        booleanBuildConfigField("AVAILABILITY_STATUS_ENABLED", false)
 
         // config fields for action URLs / deep links
         stringBuildConfigField("uriScheme", "threema")
@@ -221,7 +243,7 @@ android {
             stringBuildConfigField("MEDIA_PATH", "ThreemaWork")
             stringBuildConfigField("WORK_SERVER_URL", "https://apip-work.threema.ch/")
             stringBuildConfigField("WORK_SERVER_IPV6_URL", "https://ds-apip-work.threema.ch/")
-            stringBuildConfigField("APP_RATING_URL", "https://threema.ch/app-rating/android-work/{rating}")
+            stringBuildConfigField("APP_RATING_URL", "https://threema.com/app-rating/android-work/{rating}")
             stringBuildConfigField("LOG_TAG", "3mawrk")
             stringBuildConfigField("DEFAULT_APP_THEME", "2")
 
@@ -251,7 +273,10 @@ android {
             stringBuildConfigField("DIRECTORY_SERVER_IPV6_URL", "https://ds-apip.test.threema.ch/")
             stringBuildConfigField("MEDIATOR_SERVER_URL", "wss://mediator-{deviceGroupIdPrefix4}.test.threema.ch/{deviceGroupIdPrefix8}")
             stringBuildConfigField("AVATAR_FETCH_URL", "https://avatar.test.threema.ch/")
-            stringBuildConfigField("APP_RATING_URL", "https://test.threema.ch/app-rating/android/{rating}")
+            stringBuildConfigField("APP_RATING_URL", "https://test.threema.com/app-rating/android/{rating}")
+            stringBuildConfigField("MAP_STYLES_URL", "https://map.test.threema.ch/styles/threema/style.json")
+            stringBuildConfigField("MAP_POI_AROUND_URL", "https://poi.test.threema.ch/around/{latitude}/{longitude}/{radius}/")
+            stringBuildConfigField("MAP_POI_NAMES_URL", "https://poi.test.threema.ch/names/{latitude}/{longitude}/{query}/")
             stringBuildConfigField("BLOB_MIRROR_SERVER_URL", "https://blob-mirror-{deviceGroupIdPrefix4}.test.threema.ch/{deviceGroupIdPrefix8}")
         }
         create("sandbox_work") {
@@ -278,7 +303,10 @@ android {
             stringBuildConfigField("WORK_SERVER_IPV6_URL", "https://ds-apip-work.test.threema.ch/")
             stringBuildConfigField("MEDIATOR_SERVER_URL", "wss://mediator-{deviceGroupIdPrefix4}.test.threema.ch/{deviceGroupIdPrefix8}")
             stringBuildConfigField("AVATAR_FETCH_URL", "https://avatar.test.threema.ch/")
-            stringBuildConfigField("APP_RATING_URL", "https://test.threema.ch/app-rating/android-work/{rating}")
+            stringBuildConfigField("APP_RATING_URL", "https://test.threema.com/app-rating/android-work/{rating}")
+            stringBuildConfigField("MAP_STYLES_URL", "https://map.test.threema.ch/styles/threema/style.json")
+            stringBuildConfigField("MAP_POI_AROUND_URL", "https://poi.test.threema.ch/around/{latitude}/{longitude}/{radius}/")
+            stringBuildConfigField("MAP_POI_NAMES_URL", "https://poi.test.threema.ch/names/{latitude}/{longitude}/{query}/")
             stringBuildConfigField("LOG_TAG", "3mawrk")
             stringBuildConfigField("DEFAULT_APP_THEME", "2")
             stringBuildConfigField("BLOB_MIRROR_SERVER_URL", "https://blob-mirror-{deviceGroupIdPrefix4}.test.threema.ch/{deviceGroupIdPrefix8}")
@@ -360,7 +388,10 @@ android {
             stringBuildConfigField("WORK_SERVER_IPV6_URL", "https://ds-apip-work.test.threema.ch/")
             stringBuildConfigField("MEDIATOR_SERVER_URL", "wss://mediator-{deviceGroupIdPrefix4}.test.threema.ch/{deviceGroupIdPrefix8}")
             stringBuildConfigField("AVATAR_FETCH_URL", "https://avatar.test.threema.ch/")
-            stringBuildConfigField("APP_RATING_URL", "https://test.threema.ch/app-rating/android-work/{rating}")
+            stringBuildConfigField("APP_RATING_URL", "https://test.threema.com/app-rating/android-work/{rating}")
+            stringBuildConfigField("MAP_STYLES_URL", "https://map.test.threema.ch/styles/threema/style.json")
+            stringBuildConfigField("MAP_POI_AROUND_URL", "https://poi.test.threema.ch/around/{latitude}/{longitude}/{radius}/")
+            stringBuildConfigField("MAP_POI_NAMES_URL", "https://poi.test.threema.ch/names/{latitude}/{longitude}/{query}/")
             stringBuildConfigField("LOG_TAG", "3mablue")
             stringBuildConfigField("BLOB_MIRROR_SERVER_URL", "https://blob-mirror-{deviceGroupIdPrefix4}.test.threema.ch/{deviceGroupIdPrefix8}")
 
@@ -390,7 +421,7 @@ android {
             stringBuildConfigField("MEDIA_PATH", "ThreemaWork")
             stringBuildConfigField("WORK_SERVER_URL", "https://apip-work.threema.ch/")
             stringBuildConfigField("WORK_SERVER_IPV6_URL", "https://ds-apip-work.threema.ch/")
-            stringBuildConfigField("APP_RATING_URL", "https://threema.ch/app-rating/android-work/{rating}")
+            stringBuildConfigField("APP_RATING_URL", "https://threema.com/app-rating/android-work/{rating}")
             stringBuildConfigField("LOG_TAG", "3mawrk")
             stringBuildConfigField("DEFAULT_APP_THEME", "2")
 
@@ -602,6 +633,8 @@ android {
                 setOf(
                     "META-INF/DEPENDENCIES.txt",
                     "META-INF/LICENSE.txt",
+                    "META-INF/LICENSE.md",
+                    "META-INF/LICENSE-notice.md",
                     "META-INF/NOTICE.txt",
                     "META-INF/NOTICE",
                     "META-INF/LICENSE",
@@ -725,6 +758,7 @@ dependencies {
     coreLibraryDesugaring(libs.desugarJdkLibs)
 
     implementation(project(":domain"))
+    implementation(project(":common"))
 
     implementation(libs.sqlcipher.android)
 
@@ -775,11 +809,13 @@ dependencies {
     implementation(libs.androidx.sharetarget)
     implementation(libs.androidx.room.runtime)
     implementation(libs.androidx.window)
+    implementation(libs.androidx.splashscreen)
     ksp(libs.androidx.room.compiler)
 
     // Jetpack Compose
     implementation(platform(libs.compose.bom))
     implementation(libs.androidx.material3)
+    implementation(libs.androidx.materialIconsExtended)
     implementation(libs.androidx.ui.tooling.preview)
     implementation(libs.androidx.activity.compose)
     implementation(libs.androidx.lifecycle.viewmodel.compose)
@@ -841,6 +877,7 @@ dependencies {
     testImplementation(libs.mockito.powermock.junit4)
 
     testImplementation(libs.mockk)
+    androidTestImplementation(libs.mockkAndroid)
 
     // add JSON support to tests without mocking
     testImplementation(libs.json)
@@ -985,6 +1022,11 @@ androidStem {
     includeLocalizedOnlyTemplates = true
 }
 
+tasks.withType<Test> {
+    // Necessary to load the dynamic libthreema library in unit tests
+    systemProperty("jna.library.path", "${project.projectDir}/../domain/libthreema/target/release")
+}
+
 // Set up Gradle tasks to fetch screenshots on UI test failures
 // See https://medium.com/stepstone-tech/how-to-capture-screenshots-for-failed-ui-tests-9927eea6e1e4
 val reportsDirectory = "${layout.buildDirectory}/reports/androidTests/connected"

+ 11 - 17
app/src/androidTest/java/ch/threema/app/TestCoreServiceManager.kt

@@ -21,14 +21,12 @@
 
 package ch.threema.app
 
-import androidx.annotation.AnyThread
+import androidx.annotation.WorkerThread
 import ch.threema.app.managers.CoreServiceManager
 import ch.threema.app.managers.ServiceManager
-import ch.threema.app.multidevice.LinkedDevice
 import ch.threema.app.multidevice.MultiDeviceManager
 import ch.threema.app.multidevice.PersistedMultiDeviceProperties
 import ch.threema.app.multidevice.linking.DeviceLinkingStatus
-import ch.threema.app.multidevice.unlinking.DropDeviceResult
 import ch.threema.app.services.ContactService
 import ch.threema.app.services.UserService
 import ch.threema.app.stores.IdentityStore
@@ -48,6 +46,7 @@ import ch.threema.domain.protocol.connection.d2m.socket.D2mSocketCloseListener
 import ch.threema.domain.protocol.connection.data.D2dMessage
 import ch.threema.domain.protocol.connection.data.D2mProtocolVersion
 import ch.threema.domain.protocol.connection.data.DeviceId
+import ch.threema.domain.protocol.connection.data.InboundD2mMessage.DevicesInfo
 import ch.threema.domain.protocol.csp.fs.ForwardSecurityMessageProcessor
 import ch.threema.domain.protocol.multidevice.MultiDeviceKeys
 import ch.threema.domain.protocol.multidevice.MultiDeviceProperties
@@ -57,9 +56,8 @@ import ch.threema.domain.taskmanager.Task
 import ch.threema.domain.taskmanager.TaskArchiver
 import ch.threema.domain.taskmanager.TaskCodec
 import ch.threema.domain.taskmanager.TaskManager
-import ch.threema.storage.DatabaseServiceNew
+import ch.threema.storage.DatabaseService
 import ch.threema.testhelpers.MUST_NOT_BE_CALLED
-import kotlin.time.Duration
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Deferred
@@ -71,7 +69,7 @@ import kotlinx.coroutines.runBlocking
 
 class TestCoreServiceManager(
     override val version: AppVersion,
-    override val databaseService: DatabaseServiceNew,
+    override val databaseService: DatabaseService,
     override val preferenceStore: PreferenceStoreInterface,
     override val taskArchiver: TaskArchiver = TestTaskArchiver(),
     override val deviceCookieManager: DeviceCookieManager = TestDeviceCookieManager(),
@@ -166,8 +164,8 @@ class TestMultiDeviceManager(
     override val propertiesProvider: MultiDevicePropertyProvider = TestMultiDevicePropertyProvider,
     override val socketCloseListener: D2mSocketCloseListener = D2mSocketCloseListener { },
 ) : MultiDeviceManager {
-    @AnyThread
-    override suspend fun deactivate(serviceManager: ServiceManager, handle: ActiveTaskCodec) {
+    @WorkerThread
+    override fun removeMultiDeviceLocally(serviceManager: ServiceManager) {
         MUST_NOT_BE_CALLED()
     }
 
@@ -183,15 +181,7 @@ class TestMultiDeviceManager(
         MUST_NOT_BE_CALLED()
     }
 
-    override suspend fun dropDevice(
-        deviceId: DeviceId,
-        taskCreator: TaskCreator,
-        timeout: Duration,
-    ): DropDeviceResult {
-        MUST_NOT_BE_CALLED()
-    }
-
-    override suspend fun loadLinkedDevices(taskCreator: TaskCreator): Result<List<LinkedDevice>> {
+    override suspend fun loadLinkedDevices(taskCreator: TaskCreator): Result<Map<DeviceId, DevicesInfo.AugmentedDeviceInfo>> {
         MUST_NOT_BE_CALLED()
     }
 
@@ -212,6 +202,10 @@ class TestMultiDeviceManager(
     ) {
         MUST_NOT_BE_CALLED()
     }
+
+    override fun enableForwardSecurity(serviceManager: ServiceManager) {
+        MUST_NOT_BE_CALLED()
+    }
 }
 
 class TestNonceStore : NonceStore {

+ 1 - 1
app/src/androidTest/java/ch/threema/app/backuprestore/csv/BackupServiceTest.java

@@ -381,7 +381,7 @@ public class BackupServiceTest {
                 serviceManager.getExcludedSyncIdentitiesService(),
                 serviceManager.getDHSessionStore(),
                 serviceManager.getNotificationService(),
-                serviceManager.getDatabaseServiceNew()
+                serviceManager.getDatabaseService()
             )
         );
     }

+ 2 - 1
app/src/androidTest/java/ch/threema/app/contacts/AddOrUpdateContactBackgroundTaskTest.kt

@@ -36,6 +36,7 @@ import ch.threema.app.asynctasks.InvalidThreemaId
 import ch.threema.app.asynctasks.RemotePublicKeyMismatch
 import ch.threema.app.asynctasks.UserIdentity
 import ch.threema.app.managers.CoreServiceManager
+import ch.threema.app.utils.AppVersionProvider
 import ch.threema.app.utils.ConfigUtils
 import ch.threema.app.utils.executor.BackgroundExecutor
 import ch.threema.data.TestDatabaseService
@@ -73,7 +74,7 @@ class AddOrUpdateContactBackgroundTaskTest {
         databaseService = TestDatabaseService()
         val serviceManager = ThreemaApplication.requireServiceManager()
         coreServiceManager = TestCoreServiceManager(
-            version = ThreemaApplication.getAppVersion(),
+            version = AppVersionProvider.appVersion,
             databaseService = databaseService,
             preferenceStore = serviceManager.preferenceStore,
         )

+ 14 - 19
app/src/androidTest/java/ch/threema/app/contacts/MarkContactAsDeletedBackgroundTaskTest.kt

@@ -21,7 +21,7 @@
 
 package ch.threema.app.contacts
 
-import androidx.annotation.AnyThread
+import androidx.annotation.WorkerThread
 import ch.threema.app.DangerousTest
 import ch.threema.app.TestCoreServiceManager
 import ch.threema.app.TestMultiDevicePropertyProvider
@@ -32,15 +32,14 @@ import ch.threema.app.asynctasks.DeleteContactServices
 import ch.threema.app.asynctasks.MarkContactAsDeletedBackgroundTask
 import ch.threema.app.managers.CoreServiceManager
 import ch.threema.app.managers.ServiceManager
-import ch.threema.app.multidevice.LinkedDevice
 import ch.threema.app.multidevice.MultiDeviceManager
 import ch.threema.app.multidevice.PersistedMultiDeviceProperties
 import ch.threema.app.multidevice.linking.DeviceLinkingStatus
-import ch.threema.app.multidevice.unlinking.DropDeviceResult
 import ch.threema.app.services.ContactService
 import ch.threema.app.services.UserService
 import ch.threema.app.tasks.ReflectContactSyncUpdateTask
 import ch.threema.app.tasks.TaskCreator
+import ch.threema.app.utils.AppVersionProvider
 import ch.threema.app.utils.executor.BackgroundExecutor
 import ch.threema.data.TestDatabaseService
 import ch.threema.data.models.ContactModelData
@@ -56,6 +55,7 @@ import ch.threema.domain.models.VerificationLevel
 import ch.threema.domain.models.WorkVerificationLevel
 import ch.threema.domain.protocol.connection.d2m.socket.D2mSocketCloseListener
 import ch.threema.domain.protocol.connection.data.DeviceId
+import ch.threema.domain.protocol.connection.data.InboundD2mMessage.DevicesInfo
 import ch.threema.domain.protocol.csp.fs.ForwardSecurityMessageProcessor
 import ch.threema.domain.taskmanager.ActiveTaskCodec
 import ch.threema.domain.taskmanager.QueueSendCompleteListener
@@ -66,17 +66,16 @@ import ch.threema.storage.models.ContactModel.AcquaintanceLevel
 import ch.threema.testhelpers.MUST_NOT_BE_CALLED
 import com.neilalexander.jnacl.NaCl
 import java.util.Date
+import kotlin.test.BeforeTest
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertIs
 import kotlin.test.assertNotNull
 import kotlin.test.assertTrue
-import kotlin.time.Duration
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.test.runTest
-import org.junit.Before
 
 @DangerousTest
 class MarkContactAsDeletedBackgroundTaskTest {
@@ -113,8 +112,8 @@ class MarkContactAsDeletedBackgroundTaskTest {
         override val propertiesProvider = TestMultiDevicePropertyProvider
         override val socketCloseListener: D2mSocketCloseListener = D2mSocketCloseListener { }
 
-        @AnyThread
-        override suspend fun deactivate(serviceManager: ServiceManager, handle: ActiveTaskCodec) {
+        @WorkerThread
+        override fun removeMultiDeviceLocally(serviceManager: ServiceManager) {
             MUST_NOT_BE_CALLED()
         }
 
@@ -130,15 +129,7 @@ class MarkContactAsDeletedBackgroundTaskTest {
             MUST_NOT_BE_CALLED()
         }
 
-        override suspend fun dropDevice(
-            deviceId: DeviceId,
-            taskCreator: TaskCreator,
-            timeout: Duration,
-        ): DropDeviceResult {
-            MUST_NOT_BE_CALLED()
-        }
-
-        override suspend fun loadLinkedDevices(taskCreator: TaskCreator): Result<List<LinkedDevice>> {
+        override suspend fun loadLinkedDevices(taskCreator: TaskCreator): Result<Map<DeviceId, DevicesInfo.AugmentedDeviceInfo>> {
             MUST_NOT_BE_CALLED()
         }
 
@@ -159,6 +150,10 @@ class MarkContactAsDeletedBackgroundTaskTest {
         ) {
             MUST_NOT_BE_CALLED()
         }
+
+        override fun enableForwardSecurity(serviceManager: ServiceManager) {
+            MUST_NOT_BE_CALLED()
+        }
     }
 
     private lateinit var coreServiceManager: CoreServiceManager
@@ -190,13 +185,13 @@ class MarkContactAsDeletedBackgroundTaskTest {
         notificationTriggerPolicyOverride = null,
     )
 
-    @Before
+    @BeforeTest
     fun before() {
         databaseService = TestDatabaseService()
         val serviceManager = ThreemaApplication.requireServiceManager()
         testTaskCodec = TransactionAckTaskCodec()
         coreServiceManager = TestCoreServiceManager(
-            version = ThreemaApplication.getAppVersion(),
+            version = AppVersionProvider.appVersion,
             databaseService = databaseService,
             preferenceStore = serviceManager.preferenceStore,
             multiDeviceManager = multiDeviceManager,
@@ -214,7 +209,7 @@ class MarkContactAsDeletedBackgroundTaskTest {
             serviceManager.excludedSyncIdentitiesService,
             serviceManager.dhSessionStore,
             serviceManager.notificationService,
-            serviceManager.databaseServiceNew,
+            serviceManager.databaseService,
         )
         contactModelRepository = ModelRepositories(coreServiceManager).contacts
 

+ 4 - 3
app/src/androidTest/java/ch/threema/app/contacts/ReflectedContactSyncTaskTest.kt

@@ -27,6 +27,7 @@ import ch.threema.app.TestMultiDeviceManager
 import ch.threema.app.TestTaskManager
 import ch.threema.app.ThreemaApplication
 import ch.threema.app.processors.reflectedd2dsync.ReflectedContactSyncTask
+import ch.threema.app.utils.AppVersionProvider
 import ch.threema.data.TestDatabaseService
 import ch.threema.data.models.ContactModel
 import ch.threema.data.models.ContactModelData
@@ -57,6 +58,7 @@ import ch.threema.storage.models.ContactModel.AcquaintanceLevel
 import com.google.protobuf.kotlin.toByteString
 import com.neilalexander.jnacl.NaCl
 import java.util.Date
+import kotlin.test.BeforeTest
 import kotlin.test.Test
 import kotlin.test.assertContentEquals
 import kotlin.test.assertEquals
@@ -64,7 +66,6 @@ import kotlin.test.assertNull
 import kotlin.test.assertTrue
 import kotlin.test.fail
 import kotlinx.coroutines.runBlocking
-import org.junit.Before
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
@@ -101,12 +102,12 @@ class ReflectedContactSyncTaskTest {
         notificationTriggerPolicyOverride = null,
     )
 
-    @Before
+    @BeforeTest
     fun before() {
         databaseService = TestDatabaseService()
         taskCodec = TransactionAckTaskCodec()
         coreServiceManager = TestCoreServiceManager(
-            version = ThreemaApplication.getAppVersion(),
+            version = AppVersionProvider.appVersion,
             databaseService = databaseService,
             preferenceStore = ThreemaApplication.requireServiceManager().preferenceStore,
             multiDeviceManager = TestMultiDeviceManager(

+ 6 - 7
app/src/androidTest/java/ch/threema/app/edithistory/EditHistoryTest.kt

@@ -25,13 +25,13 @@ import androidx.test.core.app.launchActivity
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import ch.threema.app.DangerousTest
-import ch.threema.app.activities.HomeActivity
 import ch.threema.app.asynctasks.AndroidContactLinkPolicy
 import ch.threema.app.asynctasks.ContactSyncPolicy
 import ch.threema.app.asynctasks.DeleteContactServices
 import ch.threema.app.asynctasks.EmptyOrDeleteConversationsAsyncTask
 import ch.threema.app.asynctasks.MarkContactAsDeletedBackgroundTask
 import ch.threema.app.groupflows.GroupLeaveIntent
+import ch.threema.app.home.HomeActivity
 import ch.threema.app.processors.MessageProcessorProvider
 import ch.threema.app.services.ContactService
 import ch.threema.app.services.GroupService
@@ -50,18 +50,18 @@ import ch.threema.domain.protocol.csp.messages.GroupDeleteMessage
 import ch.threema.domain.protocol.csp.messages.GroupEditMessage
 import ch.threema.domain.protocol.csp.messages.GroupTextMessage
 import ch.threema.domain.protocol.csp.messages.TextMessage
-import ch.threema.storage.DatabaseServiceNew
+import ch.threema.storage.DatabaseService
 import ch.threema.storage.factories.GroupMessageModelFactory
 import ch.threema.storage.factories.MessageModelFactory
 import ch.threema.storage.models.AbstractMessageModel
 import ch.threema.storage.models.GroupMessageModel
 import ch.threema.storage.models.MessageModel
 import java.util.Date
+import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertNotNull
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.test.runTest
-import org.junit.Test
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
@@ -71,7 +71,7 @@ class EditHistoryTest : MessageProcessorProvider() {
     private val messageService: MessageService by lazy { serviceManager.messageService }
     private val contactService: ContactService by lazy { serviceManager.contactService }
     private val groupService: GroupService by lazy { serviceManager.groupService }
-    private val databaseService: DatabaseServiceNew by lazy { serviceManager.databaseServiceNew }
+    private val databaseService: DatabaseService by lazy { serviceManager.databaseService }
     private val messageModelFactory: MessageModelFactory by lazy { databaseService.messageModelFactory }
     private val groupMessageModelFactory: GroupMessageModelFactory by lazy { databaseService.groupMessageModelFactory }
     private val editHistoryDao: EditHistoryDao by lazy { EditHistoryDaoImpl(databaseService) }
@@ -308,7 +308,6 @@ class EditHistoryTest : MessageProcessorProvider() {
         )
         assertNotNull(groupModel)
         serviceManager.groupFlowDispatcher.runLeaveGroupFlow(
-            fragmentManager = null,
             intent = GroupLeaveIntent.LEAVE_AND_REMOVE,
             groupModel = groupModel,
         ).await()
@@ -339,7 +338,7 @@ class EditHistoryTest : MessageProcessorProvider() {
             text = "Original Text"
             fromIdentity = contactA.identity
             toIdentity = myContact.identity
-            messageId = MessageId()
+            messageId = MessageId.random()
         }
 
         processMessage(message, contactA.identityStore)
@@ -380,7 +379,7 @@ class EditHistoryTest : MessageProcessorProvider() {
             groupCreator = groupA.groupCreator.identity
             fromIdentity = contactA.identity
             toIdentity = myContact.identity
-            messageId = MessageId()
+            messageId = MessageId.random()
         }
 
         processMessage(message, contactA.identityStore)

+ 4 - 4
app/src/androidTest/java/ch/threema/app/emojis/MarkupParserTest.java

@@ -217,10 +217,10 @@ public class MarkupParserTest {
 
     @Test
     public void atWordBoundaries6() {
-        final SpannableStringBuilder parsed = Utils.parse("_<a href=\"https://threema.ch\">Threema</a>_");
-        assertEquals("<a href=\"https://threema.ch\">Threema</a>", parsed.toString());
+        final SpannableStringBuilder parsed = Utils.parse("_<a href=\"https://threema.com\">Threema</a>_");
+        assertEquals("<a href=\"https://threema.com\">Threema</a>", parsed.toString());
         Utils.expectSpanCount(parsed, 1);
-        Utils.expectItalicAt(parsed, 0, 40);
+        Utils.expectItalicAt(parsed, 0, 41);
     }
 
     @Test
@@ -245,7 +245,7 @@ public class MarkupParserTest {
 
     @Test
     public void onlyWordBoundaries5() {
-        Utils.expectNoSpan("<a href=\"https://threema.ch\">_Threema_</a>");
+        Utils.expectNoSpan("<a href=\"https://threema.com\">_Threema_</a>");
     }
 
     @Test

+ 47 - 22
app/src/androidTest/java/ch/threema/app/groupmanagement/CreateGroupFlowTest.kt

@@ -25,6 +25,7 @@ import android.text.format.DateUtils
 import ch.threema.app.DangerousTest
 import ch.threema.app.ThreemaApplication
 import ch.threema.app.groupflows.GroupCreateProperties
+import ch.threema.app.groupflows.GroupFlowResult
 import ch.threema.app.groupflows.ProfilePicture
 import ch.threema.app.tasks.GroupCreateTask
 import ch.threema.app.tasks.ReflectGroupSyncCreateTask
@@ -47,16 +48,15 @@ import ch.threema.domain.taskmanager.TaskCodec
 import ch.threema.storage.models.ContactModel
 import ch.threema.storage.models.GroupModel.UserState
 import java.util.Date
+import kotlin.test.BeforeTest
+import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertFalse
 import kotlin.test.assertIs
 import kotlin.test.assertNotNull
 import kotlin.test.assertNull
 import kotlin.test.assertTrue
-import kotlin.test.fail
 import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
 
 /**
  * This test asserts that the corresponding tasks have been scheduled when running the create group
@@ -92,7 +92,7 @@ class CreateGroupFlowTest : GroupFlowTest() {
         notificationTriggerPolicyOverride = null,
     )
 
-    @Before
+    @BeforeTest
     fun setup() {
         clearDatabaseAndCaches(serviceManager)
 
@@ -103,7 +103,7 @@ class CreateGroupFlowTest : GroupFlowTest() {
     }
 
     @Test
-    fun testKnownMember() = runTest {
+    fun testKnownMemberMD() = runTest {
         val memberIdentity = initialContactModelData.identity
 
         // Assert that the member exists as a contact
@@ -120,7 +120,7 @@ class CreateGroupFlowTest : GroupFlowTest() {
     }
 
     @Test
-    fun testKnownMemberNonMd() = runTest {
+    fun testKnownMemberNonMD() = runTest {
         val memberIdentity = initialContactModelData.identity
 
         // Assert that the member exists as a contact
@@ -137,7 +137,7 @@ class CreateGroupFlowTest : GroupFlowTest() {
     }
 
     @Test
-    fun testNotesGroupMd() = runTest {
+    fun testNotesGroupMD() = runTest {
         testAndAssertSuccessfulGroupCreation(
             GroupCreateProperties(
                 name = "Test",
@@ -149,7 +149,7 @@ class CreateGroupFlowTest : GroupFlowTest() {
     }
 
     @Test
-    fun testNotesGroupNonMd() = runTest {
+    fun testNotesGroupNonMD() = runTest {
         testAndAssertSuccessfulGroupCreation(
             GroupCreateProperties(
                 name = "Test",
@@ -161,13 +161,13 @@ class CreateGroupFlowTest : GroupFlowTest() {
     }
 
     @Test
-    fun testUnknownMemberMd() = runTest {
+    fun testUnknownMemberMD() = runTest {
         val unknownIdentity = "0UNKNOWN"
 
         // Assert that the identity is really unknown
         assertNull(contactModelRepository.getByIdentity(unknownIdentity))
 
-        val groupModel = testGroupCreation(
+        val groupFlowResult: GroupFlowResult = testGroupCreation(
             GroupCreateProperties(
                 name = "Test",
                 profilePicture = ProfilePicture(null as ByteArray?),
@@ -176,14 +176,40 @@ class CreateGroupFlowTest : GroupFlowTest() {
             ReflectionExpectation.REFLECTION_FAIL,
         )
 
-        assertNull(groupModel)
+        assertIs<GroupFlowResult.Failure.Other>(groupFlowResult)
+    }
+
+    @Test
+    fun shouldNotCreateGroupWhenMdActiveButConnectionIsLost() = runTest {
+        // arrange
+        val taskManager = ControlledTaskManager(emptyList())
+        val groupFlowDispatcher = getGroupFlowDispatcher(
+            setupConfig = SetupConfig.MULTI_DEVICE_ENABLED,
+            taskManager = taskManager,
+            connection = ConnectionDisconnected,
+        )
+
+        // act
+        val groupFlowResult: GroupFlowResult = groupFlowDispatcher.runCreateGroupFlow(
+            ThreemaApplication.getAppContext(),
+            GroupCreateProperties(
+                name = "Test",
+                profilePicture = ProfilePicture(null as ByteArray?),
+                members = setOf(initialContactModelData.identity),
+            ),
+        ).await()
+
+        // assert
+        assertTrue(groupFlowResult is GroupFlowResult.Failure.Network)
     }
 
     private suspend fun testAndAssertSuccessfulGroupCreation(
         groupCreateProperties: GroupCreateProperties,
         reflectionExpectation: ReflectionExpectation,
-    ): GroupModel? {
-        val groupModel = testGroupCreation(groupCreateProperties, reflectionExpectation)
+    ): GroupModel {
+        val createGroupFlowResult = testGroupCreation(groupCreateProperties, reflectionExpectation)
+        assertTrue(createGroupFlowResult is GroupFlowResult.Success)
+        val groupModel = createGroupFlowResult.groupModel
         groupModel.assertCreatedFrom(groupCreateProperties)
         groupModel.assertNewGroup()
         return groupModel
@@ -192,9 +218,10 @@ class CreateGroupFlowTest : GroupFlowTest() {
     private suspend fun testGroupCreation(
         groupCreateProperties: GroupCreateProperties,
         reflectionExpectation: ReflectionExpectation,
-    ): GroupModel? {
+    ): GroupFlowResult {
         val scheduledTaskAssertions: MutableList<(Task<*, TaskCodec>) -> Unit> = mutableListOf()
-        // If multi device is enabled, then we expect a reflection
+
+        // If multi device is enabled, then we expect the ReflectGroupSyncCreateTask to be scheduled
         if (reflectionExpectation.setupConfig == SetupConfig.MULTI_DEVICE_ENABLED) {
             scheduledTaskAssertions.add { task ->
                 assertIs<ReflectGroupSyncCreateTask>(task)
@@ -216,23 +243,21 @@ class CreateGroupFlowTest : GroupFlowTest() {
         val groupFlowDispatcher = getGroupFlowDispatcher(
             reflectionExpectation.setupConfig,
             taskManager,
+            ConnectionLoggedIn,
         )
 
         // Run create group flow
-        val groupModel = groupFlowDispatcher.runCreateGroupFlow(
-            null,
+        val groupFlowResult: GroupFlowResult = groupFlowDispatcher.runCreateGroupFlow(
             ThreemaApplication.getAppContext(),
             groupCreateProperties,
         ).await()
 
         // Assert that all expected tasks have been scheduled
-        taskManager.pendingTaskAssertions.size.let { size ->
-            if (size > 0) {
-                fail("There are $size pending task assertions left")
-            }
+        assert(taskManager.pendingTaskAssertions.isEmpty()) {
+            "There are ${taskManager.pendingTaskAssertions} pending task assertions left"
         }
 
-        return groupModel
+        return groupFlowResult
     }
 
     private fun GroupModel?.assertCreatedFrom(groupCreateProperties: GroupCreateProperties) {

+ 33 - 15
app/src/androidTest/java/ch/threema/app/groupmanagement/DisbandGroupFlowTest.kt

@@ -23,6 +23,7 @@ package ch.threema.app.groupmanagement
 
 import ch.threema.app.DangerousTest
 import ch.threema.app.groupflows.GroupDisbandIntent
+import ch.threema.app.groupflows.GroupFlowResult
 import ch.threema.app.tasks.OutgoingGroupDisbandTask
 import ch.threema.app.tasks.ReflectGroupSyncDeleteTask
 import ch.threema.app.tasks.ReflectLocalGroupLeaveOrDisband
@@ -45,17 +46,15 @@ import ch.threema.domain.taskmanager.TaskCodec
 import ch.threema.storage.models.ContactModel.AcquaintanceLevel
 import ch.threema.storage.models.GroupModel.UserState
 import java.util.Date
+import kotlin.test.BeforeTest
+import kotlin.test.Test
 import kotlin.test.assertEquals
-import kotlin.test.assertFalse
 import kotlin.test.assertIs
 import kotlin.test.assertNotEquals
 import kotlin.test.assertNotNull
 import kotlin.test.assertNull
-import kotlin.test.assertTrue
 import kotlin.test.fail
 import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
 
 @DangerousTest
 class DisbandGroupFlowTest : GroupFlowTest() {
@@ -125,7 +124,7 @@ class DisbandGroupFlowTest : GroupFlowTest() {
         otherMembers = emptySet(),
     )
 
-    @Before
+    @BeforeTest
     fun setup() {
         clearDatabaseAndCaches(serviceManager)
 
@@ -342,14 +341,34 @@ class DisbandGroupFlowTest : GroupFlowTest() {
         )
     }
 
+    @Test
+    fun shouldNotDisbandGroupWhenMdActiveButConnectionIsLost() = runTest {
+        // arrange
+        val groupModel = groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
+        val taskManager = ControlledTaskManager(emptyList())
+        val groupFlowDispatcher = getGroupFlowDispatcher(
+            setupConfig = SetupConfig.MULTI_DEVICE_ENABLED,
+            taskManager = taskManager,
+            connection = ConnectionDisconnected,
+        )
+
+        // act
+        val groupFlowResult = groupFlowDispatcher
+            .runDisbandGroupFlow(GroupDisbandIntent.DISBAND, groupModel!!)
+            .await()
+
+        // assert
+        assertIs<GroupFlowResult.Failure.Network>(groupFlowResult)
+    }
+
     private suspend fun assertSuccessfulDisband(
         groupModel: GroupModel,
         intent: GroupDisbandIntent,
         reflectionExpectation: ReflectionExpectation,
     ) {
-        assertTrue {
-            runGroupDisband(groupModel, intent, reflectionExpectation)
-        }
+        assertIs<GroupFlowResult.Success>(
+            runGroupDisband(groupModel, intent, reflectionExpectation),
+        )
 
         when (intent) {
             GroupDisbandIntent.DISBAND -> assertEquals(
@@ -367,9 +386,9 @@ class DisbandGroupFlowTest : GroupFlowTest() {
         reflectionExpectation: ReflectionExpectation,
     ) {
         val groupModelDataBefore = groupModel.data.value
-        assertFalse {
-            runGroupDisband(groupModel, intent, reflectionExpectation)
-        }
+        assertIs<GroupFlowResult.Failure>(
+            runGroupDisband(groupModel, intent, reflectionExpectation),
+        )
         val groupModelDataAfter = groupModel.data.value
         // Assert that the group model has not changed
         assertEquals(groupModelDataBefore, groupModelDataAfter)
@@ -379,7 +398,7 @@ class DisbandGroupFlowTest : GroupFlowTest() {
         groupModel: GroupModel,
         intent: GroupDisbandIntent,
         reflectionExpectation: ReflectionExpectation,
-    ): Boolean {
+    ): GroupFlowResult {
         val groupModelData = groupModel.data.value
 
         // Prepare task manager and group flow dispatcher
@@ -392,8 +411,7 @@ class DisbandGroupFlowTest : GroupFlowTest() {
         )
 
         // Run disband group flow
-        val result = groupFlowDispatcher.runDisbandGroupFlow(
-            null,
+        val groupFlowResult = groupFlowDispatcher.runDisbandGroupFlow(
             intent,
             groupModel,
         ).await()
@@ -404,7 +422,7 @@ class DisbandGroupFlowTest : GroupFlowTest() {
             }
         }
 
-        return result
+        return groupFlowResult
     }
 
     private fun getExpectedTaskAssertions(

+ 1 - 1
app/src/androidTest/java/ch/threema/app/groupmanagement/GroupControlTest.kt

@@ -29,7 +29,7 @@ import androidx.test.espresso.action.ViewActions
 import androidx.test.espresso.intent.Intents
 import androidx.test.espresso.matcher.ViewMatchers
 import ch.threema.app.R
-import ch.threema.app.activities.HomeActivity
+import ch.threema.app.home.HomeActivity
 import ch.threema.app.processors.MessageProcessorProvider
 import ch.threema.app.testutils.TestHelpers.TestGroup
 import ch.threema.domain.protocol.csp.messages.AbstractGroupMessage

+ 2 - 2
app/src/androidTest/java/ch/threema/app/groupmanagement/GroupConversationListTest.kt

@@ -24,8 +24,8 @@ package ch.threema.app.groupmanagement
 import androidx.recyclerview.widget.RecyclerView
 import androidx.test.core.app.ActivityScenario
 import ch.threema.app.R
-import ch.threema.app.activities.HomeActivity
 import ch.threema.app.adapters.MessageListAdapter
+import ch.threema.app.home.HomeActivity
 import ch.threema.app.testutils.TestHelpers.TestGroup
 import ch.threema.domain.protocol.csp.messages.AbstractGroupMessage
 import junit.framework.TestCase
@@ -62,7 +62,7 @@ abstract class GroupConversationListTest<T : AbstractGroupMessage> : GroupContro
 
         val actualGroupNames = (0 until adapter.itemCount)
             .mapNotNull { adapter.getEntity(it) }
-            .map { it.receiver.displayName }
+            .map { it.messageReceiver.displayName }
             .toSet()
 
         TestCase.assertEquals(errorMessage, expectedGroupNames, actualGroupNames)

+ 44 - 1
app/src/androidTest/java/ch/threema/app/groupmanagement/GroupFlowTest.kt

@@ -24,6 +24,9 @@ package ch.threema.app.groupmanagement
 import ch.threema.app.TestMultiDeviceManager
 import ch.threema.app.ThreemaApplication
 import ch.threema.app.services.GroupFlowDispatcher
+import ch.threema.domain.protocol.connection.ConnectionState
+import ch.threema.domain.protocol.connection.ConnectionStateListener
+import ch.threema.domain.protocol.connection.ServerConnection
 import ch.threema.domain.taskmanager.TaskManager
 
 enum class SetupConfig {
@@ -72,6 +75,7 @@ abstract class GroupFlowTest {
     protected fun getGroupFlowDispatcher(
         setupConfig: SetupConfig,
         taskManager: TaskManager,
+        connection: ServerConnection = ConnectionLoggedIn,
     ) = GroupFlowDispatcher(
         serviceManager.modelRepositories.contacts,
         serviceManager.modelRepositories.groups,
@@ -92,7 +96,46 @@ abstract class GroupFlowTest {
         serviceManager.apiService,
         serviceManager.apiConnector,
         serviceManager.fileService,
-        serviceManager.databaseServiceNew,
+        serviceManager.databaseService,
         taskManager,
+        connection,
     )
+
+    data object ConnectionDisconnected : ServerConnection {
+
+        override val isRunning: Boolean = false
+
+        override val connectionState: ConnectionState = ConnectionState.DISCONNECTED
+
+        override val isNewConnectionSession: Boolean = false
+
+        override fun disableReconnect() {}
+
+        override fun start() {}
+
+        override fun stop() {}
+
+        override fun addConnectionStateListener(listener: ConnectionStateListener) {}
+
+        override fun removeConnectionStateListener(listener: ConnectionStateListener) {}
+    }
+
+    data object ConnectionLoggedIn : ServerConnection {
+
+        override val isRunning: Boolean = true
+
+        override val connectionState: ConnectionState = ConnectionState.LOGGEDIN
+
+        override val isNewConnectionSession: Boolean = true
+
+        override fun disableReconnect() {}
+
+        override fun start() {}
+
+        override fun stop() {}
+
+        override fun addConnectionStateListener(listener: ConnectionStateListener) {}
+
+        override fun removeConnectionStateListener(listener: ConnectionStateListener) {}
+    }
 }

+ 16 - 14
app/src/androidTest/java/ch/threema/app/groupmanagement/GroupResyncFlowTest.kt

@@ -22,6 +22,7 @@
 package ch.threema.app.groupmanagement
 
 import ch.threema.app.DangerousTest
+import ch.threema.app.groupflows.GroupFlowResult
 import ch.threema.app.tasks.ActiveGroupStateResyncTask
 import ch.threema.app.testutils.TestHelpers
 import ch.threema.app.testutils.clearDatabaseAndCaches
@@ -42,14 +43,12 @@ import ch.threema.domain.taskmanager.TaskCodec
 import ch.threema.storage.models.ContactModel
 import ch.threema.storage.models.GroupModel.UserState
 import java.util.Date
-import kotlin.test.assertFalse
+import kotlin.test.BeforeTest
+import kotlin.test.Test
 import kotlin.test.assertIs
 import kotlin.test.assertNotNull
-import kotlin.test.assertTrue
 import kotlin.test.fail
 import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
 
 @DangerousTest
 class GroupResyncFlowTest : GroupFlowTest() {
@@ -100,7 +99,7 @@ class GroupResyncFlowTest : GroupFlowTest() {
         name = "ExistingGroup",
     )
 
-    @Before
+    @BeforeTest
     fun setup() {
         clearDatabaseAndCaches(serviceManager)
 
@@ -150,21 +149,24 @@ class GroupResyncFlowTest : GroupFlowTest() {
         groupModel: GroupModel,
         setupConfig: SetupConfig,
     ) {
-        assertTrue {
-            runGroupResync(groupModel, setupConfig)
-        }
+        assertIs<GroupFlowResult.Success>(
+            runGroupResync(groupModel, setupConfig),
+        )
     }
 
     private suspend fun assertUnsuccessfulGroupResync(
         groupModel: GroupModel,
         setupConfig: SetupConfig,
     ) {
-        assertFalse {
-            runGroupResync(groupModel, setupConfig)
-        }
+        assertIs<GroupFlowResult.Failure>(
+            runGroupResync(groupModel, setupConfig),
+        )
     }
 
-    private suspend fun runGroupResync(groupModel: GroupModel, setupConfig: SetupConfig): Boolean {
+    private suspend fun runGroupResync(
+        groupModel: GroupModel,
+        setupConfig: SetupConfig,
+    ): GroupFlowResult {
         val groupModelData = groupModel.data.value
 
         // Prepare task manager and group flow dispatcher
@@ -177,7 +179,7 @@ class GroupResyncFlowTest : GroupFlowTest() {
         )
 
         // Run group resync flow
-        val result = groupFlowDispatcher.runGroupResyncFlow(groupModel).await()
+        val groupFowResult = groupFlowDispatcher.runGroupResyncFlow(groupModel).await()
 
         taskManager.pendingTaskAssertions.size.let { size ->
             if (size > 0) {
@@ -185,7 +187,7 @@ class GroupResyncFlowTest : GroupFlowTest() {
             }
         }
 
-        return result
+        return groupFowResult
     }
 
     private fun getExpectedTaskAssertions(groupModelData: GroupModelData?): MutableList<(Task<*, TaskCodec>) -> Unit> {

+ 4 - 4
app/src/androidTest/java/ch/threema/app/groupmanagement/IncomingGroupLeaveTest.kt

@@ -25,7 +25,7 @@ import androidx.test.core.app.launchActivity
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import ch.threema.app.DangerousTest
-import ch.threema.app.activities.HomeActivity
+import ch.threema.app.home.HomeActivity
 import ch.threema.app.listeners.GroupListener
 import ch.threema.app.managers.ListenerManager
 import ch.threema.app.testutils.TestHelpers.TestContact
@@ -37,9 +37,9 @@ import junit.framework.TestCase.assertEquals
 import junit.framework.TestCase.assertFalse
 import junit.framework.TestCase.assertTrue
 import junit.framework.TestCase.fail
+import kotlin.test.AfterTest
+import kotlin.test.Test
 import kotlinx.coroutines.test.runTest
-import org.junit.After
-import org.junit.Test
 import org.junit.runner.RunWith
 
 /**
@@ -119,7 +119,7 @@ class IncomingGroupLeaveTest : GroupControlTest<GroupLeaveMessage>() {
         assertUnsuccessfulLeave(groupA, contactB)
     }
 
-    @After
+    @AfterTest
     fun removeAllGroupListeners() {
         GroupLeaveTracker.stopAllListeners()
     }

+ 3 - 3
app/src/androidTest/java/ch/threema/app/groupmanagement/IncomingGroupNameTest.kt

@@ -32,9 +32,9 @@ import ch.threema.data.models.GroupIdentity
 import ch.threema.domain.models.GroupId
 import ch.threema.domain.protocol.csp.messages.GroupNameMessage
 import junit.framework.TestCase.*
+import kotlin.test.AfterTest
+import kotlin.test.Test
 import kotlinx.coroutines.test.runTest
-import org.junit.After
-import org.junit.Test
 import org.junit.runner.RunWith
 
 /**
@@ -165,7 +165,7 @@ class IncomingGroupNameTest : GroupConversationListTest<GroupNameMessage>() {
         // receive steps.
     }
 
-    @After
+    @AfterTest
     fun removeAllGroupListeners() {
         GroupRenameTracker.stopAllListeners()
     }

+ 5 - 5
app/src/androidTest/java/ch/threema/app/groupmanagement/IncomingGroupSetupTest.kt

@@ -43,15 +43,15 @@ import ch.threema.storage.models.GroupModel
 import com.neilalexander.jnacl.NaCl
 import java.util.Date
 import junit.framework.TestCase
+import kotlin.test.AfterTest
+import kotlin.test.Test
 import kotlin.test.assertNotNull
 import kotlin.test.assertNull
 import kotlinx.coroutines.test.runTest
-import org.junit.After
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Assert.fail
-import org.junit.Test
 import org.junit.runner.RunWith
 
 /**
@@ -497,11 +497,11 @@ class IncomingGroupSetupTest : GroupConversationListTest<GroupSetupMessage>() {
         // the database).
         assertEquals(
             expectedMemberCount,
-            serviceManager.databaseServiceNew.groupMemberModelFactory.getByGroupId(group.id).size + 1,
+            serviceManager.databaseService.groupMemberModelFactory.getByGroupId(group.id).size + 1,
         )
         assertEquals(
             expectedMemberCount,
-            serviceManager.databaseServiceNew.groupMemberModelFactory.countMembersWithoutUser(group.id)
+            serviceManager.databaseService.groupMemberModelFactory.countMembersWithoutUser(group.id)
                 .toInt() + 1,
         )
 
@@ -639,7 +639,7 @@ class IncomingGroupSetupTest : GroupConversationListTest<GroupSetupMessage>() {
         }
     }
 
-    @After
+    @AfterTest
     fun removeAllGroupListeners() {
         GroupSetupTracker.stopAllListeners()
     }

+ 1 - 1
app/src/androidTest/java/ch/threema/app/groupmanagement/IncomingGroupSyncRequestTest.kt

@@ -25,7 +25,7 @@ import androidx.test.core.app.launchActivity
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import ch.threema.app.DangerousTest
-import ch.threema.app.activities.HomeActivity
+import ch.threema.app.home.HomeActivity
 import ch.threema.app.testutils.TestHelpers.TestContact
 import ch.threema.app.testutils.TestHelpers.TestGroup
 import ch.threema.domain.protocol.csp.messages.GroupDeleteProfilePictureMessage

+ 1 - 1
app/src/androidTest/java/ch/threema/app/groupmanagement/IncomingGroupTextTest.kt

@@ -25,9 +25,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import ch.threema.app.DangerousTest
 import ch.threema.domain.protocol.csp.messages.GroupTextMessage
+import kotlin.test.Test
 import kotlinx.coroutines.runBlocking
 import org.junit.Assert
-import org.junit.Test
 import org.junit.runner.RunWith
 
 /**

+ 33 - 16
app/src/androidTest/java/ch/threema/app/groupmanagement/LeaveGroupFlowTest.kt

@@ -22,6 +22,7 @@
 package ch.threema.app.groupmanagement
 
 import ch.threema.app.DangerousTest
+import ch.threema.app.groupflows.GroupFlowResult
 import ch.threema.app.groupflows.GroupLeaveIntent
 import ch.threema.app.tasks.OutgoingGroupLeaveTask
 import ch.threema.app.tasks.ReflectGroupSyncDeleteTask
@@ -45,16 +46,14 @@ import ch.threema.domain.taskmanager.TaskCodec
 import ch.threema.storage.models.ContactModel
 import ch.threema.storage.models.GroupModel.UserState
 import java.util.Date
+import kotlin.test.BeforeTest
+import kotlin.test.Test
 import kotlin.test.assertEquals
-import kotlin.test.assertFalse
 import kotlin.test.assertIs
 import kotlin.test.assertNotNull
 import kotlin.test.assertNull
-import kotlin.test.assertTrue
 import kotlin.test.fail
 import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
 
 @DangerousTest
 class LeaveGroupFlowTest : GroupFlowTest() {
@@ -117,7 +116,7 @@ class LeaveGroupFlowTest : GroupFlowTest() {
         userState = UserState.KICKED,
     )
 
-    @Before
+    @BeforeTest
     fun setup() {
         clearDatabaseAndCaches(serviceManager)
 
@@ -389,15 +388,34 @@ class LeaveGroupFlowTest : GroupFlowTest() {
         )
     }
 
+    @Test
+    fun shouldNotLeaveGroupWhenMdActiveButConnectionIsLost() = runTest {
+        // arrange
+        val groupModel = groupModelRepository.getByGroupIdentity(initialGroupModelData.groupIdentity)
+        val taskManager = ControlledTaskManager(emptyList())
+        val groupFlowDispatcher = getGroupFlowDispatcher(
+            setupConfig = SetupConfig.MULTI_DEVICE_ENABLED,
+            taskManager = taskManager,
+            connection = ConnectionDisconnected,
+        )
+
+        // act
+        val groupFlowResult = groupFlowDispatcher
+            .runLeaveGroupFlow(GroupLeaveIntent.LEAVE, groupModel!!)
+            .await()
+
+        // assert
+        assertIs<GroupFlowResult.Failure.Network>(groupFlowResult)
+    }
+
     private suspend fun assertSuccessfulLeave(
         groupModel: GroupModel,
         intent: GroupLeaveIntent,
         reflectionExpectation: ReflectionExpectation,
     ) {
-        assertTrue {
-            runGroupLeave(groupModel, intent, reflectionExpectation)
-        }
-
+        assertIs<GroupFlowResult.Success>(
+            runGroupLeave(groupModel, intent, reflectionExpectation),
+        )
         when (intent) {
             GroupLeaveIntent.LEAVE -> assertEquals(UserState.LEFT, groupModel.data.value?.userState)
             GroupLeaveIntent.LEAVE_AND_REMOVE -> assertNull(groupModel.data.value)
@@ -410,9 +428,9 @@ class LeaveGroupFlowTest : GroupFlowTest() {
         reflectionExpectation: ReflectionExpectation,
     ) {
         val groupModelDataBefore = groupModel.data.value
-        assertFalse {
-            runGroupLeave(groupModel, intent, reflectionExpectation)
-        }
+        assertIs<GroupFlowResult.Failure>(
+            runGroupLeave(groupModel, intent, reflectionExpectation),
+        )
         val groupModelDataAfter = groupModel.data.value
         // Assert that the group model has not changed
         assertEquals(groupModelDataBefore, groupModelDataAfter)
@@ -422,7 +440,7 @@ class LeaveGroupFlowTest : GroupFlowTest() {
         groupModel: GroupModel,
         intent: GroupLeaveIntent,
         reflectionExpectation: ReflectionExpectation,
-    ): Boolean {
+    ): GroupFlowResult {
         val groupModelData = groupModel.data.value
 
         // Prepare task manager and group flow dispatcher
@@ -435,8 +453,7 @@ class LeaveGroupFlowTest : GroupFlowTest() {
         )
 
         // Run leave group flow
-        val result = groupFlowDispatcher.runLeaveGroupFlow(
-            null,
+        val groupFlowResult = groupFlowDispatcher.runLeaveGroupFlow(
             intent,
             groupModel,
         ).await()
@@ -447,7 +464,7 @@ class LeaveGroupFlowTest : GroupFlowTest() {
             }
         }
 
-        return result
+        return groupFlowResult
     }
 
     private fun getExpectedTaskAssertions(

+ 32 - 18
app/src/androidTest/java/ch/threema/app/groupmanagement/RemoveGroupFlowTest.kt

@@ -22,6 +22,7 @@
 package ch.threema.app.groupmanagement
 
 import ch.threema.app.DangerousTest
+import ch.threema.app.groupflows.GroupFlowResult
 import ch.threema.app.tasks.ReflectGroupSyncDeleteTask
 import ch.threema.app.testutils.TestHelpers
 import ch.threema.app.testutils.clearDatabaseAndCaches
@@ -42,15 +43,13 @@ import ch.threema.domain.taskmanager.TaskCodec
 import ch.threema.storage.models.ContactModel
 import ch.threema.storage.models.GroupModel.UserState
 import java.util.Date
+import kotlin.test.BeforeTest
 import kotlin.test.Test
-import kotlin.test.assertFalse
 import kotlin.test.assertIs
 import kotlin.test.assertNotNull
 import kotlin.test.assertNull
-import kotlin.test.assertTrue
 import kotlin.test.fail
 import kotlinx.coroutines.test.runTest
-import org.junit.Before
 
 @DangerousTest
 class RemoveGroupFlowTest : GroupFlowTest() {
@@ -114,7 +113,7 @@ class RemoveGroupFlowTest : GroupFlowTest() {
         userState = UserState.LEFT,
     )
 
-    @Before
+    @BeforeTest
     fun setup() {
         clearDatabaseAndCaches(serviceManager)
 
@@ -246,14 +245,33 @@ class RemoveGroupFlowTest : GroupFlowTest() {
         )
     }
 
+    @Test
+    fun shouldNotRemoveGroupWhenMdActiveButConnectionIsLost() = runTest {
+        // arrange
+        val groupModel = groupModelRepository.getByGroupIdentity(myInitialLeftGroupModelData.groupIdentity)
+        val taskManager = ControlledTaskManager(emptyList())
+        val groupFlowDispatcher = getGroupFlowDispatcher(
+            setupConfig = SetupConfig.MULTI_DEVICE_ENABLED,
+            taskManager = taskManager,
+            connection = ConnectionDisconnected,
+        )
+
+        // act
+        val groupFlowResult = groupFlowDispatcher
+            .runRemoveGroupFlow(groupModel!!)
+            .await()
+
+        // assert
+        assertIs<GroupFlowResult.Failure.Network>(groupFlowResult)
+    }
+
     private suspend fun assertSuccessfulRemove(
         groupModel: GroupModel,
         reflectionExpectation: ReflectionExpectation,
     ) {
-        assertTrue {
-            runGroupRemove(groupModel, reflectionExpectation)
-        }
-
+        assertIs<GroupFlowResult.Success>(
+            runGroupRemove(groupModel, reflectionExpectation),
+        )
         assertNull(groupModel.data.value)
     }
 
@@ -261,17 +279,16 @@ class RemoveGroupFlowTest : GroupFlowTest() {
         groupModel: GroupModel,
         reflectionExpectation: ReflectionExpectation,
     ) {
-        assertFalse {
-            runGroupRemove(groupModel, reflectionExpectation)
-        }
-
+        assertIs<GroupFlowResult.Failure>(
+            runGroupRemove(groupModel, reflectionExpectation),
+        )
         assertNotNull(groupModel.data.value)
     }
 
     private suspend fun runGroupRemove(
         groupModel: GroupModel,
         reflectionExpectation: ReflectionExpectation,
-    ): Boolean {
+    ): GroupFlowResult {
         val groupModelData = groupModel.data.value
 
         // Prepare task manager and group flow dispatcher
@@ -284,10 +301,7 @@ class RemoveGroupFlowTest : GroupFlowTest() {
         )
 
         // Run remove group flow
-        val result = groupFlowDispatcher.runRemoveGroupFlow(
-            null,
-            groupModel,
-        ).await()
+        val groupFlowResult = groupFlowDispatcher.runRemoveGroupFlow(groupModel).await()
 
         taskManager.pendingTaskAssertions.size.let { size ->
             if (size > 0) {
@@ -295,7 +309,7 @@ class RemoveGroupFlowTest : GroupFlowTest() {
             }
         }
 
-        return result
+        return groupFlowResult
     }
 
     private fun getExpectedTaskAssertions(

+ 64 - 52
app/src/androidTest/java/ch/threema/app/groupmanagement/UpdateGroupFlowTest.kt

@@ -23,6 +23,7 @@ package ch.threema.app.groupmanagement
 
 import ch.threema.app.DangerousTest
 import ch.threema.app.groupflows.GroupChanges
+import ch.threema.app.groupflows.GroupFlowResult
 import ch.threema.app.tasks.GroupUpdateTask
 import ch.threema.app.tasks.ReflectLocalGroupUpdate
 import ch.threema.app.testutils.TestHelpers
@@ -44,17 +45,16 @@ import ch.threema.domain.taskmanager.TaskCodec
 import ch.threema.storage.models.ContactModel
 import ch.threema.storage.models.GroupModel.UserState
 import java.util.Date
+import kotlin.test.BeforeTest
 import kotlin.test.Test
 import kotlin.test.assertContains
 import kotlin.test.assertEquals
-import kotlin.test.assertFalse
 import kotlin.test.assertIs
 import kotlin.test.assertNotNull
 import kotlin.test.assertNull
 import kotlin.test.assertTrue
 import kotlin.test.fail
 import kotlinx.coroutines.test.runTest
-import org.junit.Before
 
 @DangerousTest
 class UpdateGroupFlowTest : GroupFlowTest() {
@@ -148,7 +148,7 @@ class UpdateGroupFlowTest : GroupFlowTest() {
         notificationTriggerPolicyOverride = null,
     )
 
-    @Before
+    @BeforeTest
     fun setup() {
         clearDatabaseAndCaches(serviceManager)
 
@@ -167,8 +167,7 @@ class UpdateGroupFlowTest : GroupFlowTest() {
 
     @Test
     fun testGroupNameModificationMd() = runTest {
-        val groupModel =
-            groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
+        val groupModel = groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
         assertNotNull(groupModel)
         val groupChanges = GroupChanges(
             name = "NewGroupName",
@@ -186,8 +185,7 @@ class UpdateGroupFlowTest : GroupFlowTest() {
 
     @Test
     fun testGroupNameModificationNonMd() = runTest {
-        val groupModel =
-            groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
+        val groupModel = groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
         assertNotNull(groupModel)
         val groupChanges = GroupChanges(
             name = "NewGroupName",
@@ -205,8 +203,7 @@ class UpdateGroupFlowTest : GroupFlowTest() {
 
     @Test
     fun testGroupNameAndAddedMembersModificationMd() = runTest {
-        val groupModel =
-            groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
+        val groupModel = groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
         assertNotNull(groupModel)
 
         // Assert that the new member is not yet a member of the group
@@ -234,8 +231,7 @@ class UpdateGroupFlowTest : GroupFlowTest() {
 
     @Test
     fun testGroupNameAndAddedMembersModificationNonMd() = runTest {
-        val groupModel =
-            groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
+        val groupModel = groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
         assertNotNull(groupModel)
         val groupChanges = GroupChanges(
             name = "NewGroupName",
@@ -257,8 +253,7 @@ class UpdateGroupFlowTest : GroupFlowTest() {
 
     @Test
     fun testGroupNameAndRemovedMembersModificationMd() = runTest {
-        val groupModel =
-            groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
+        val groupModel = groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
         assertNotNull(groupModel)
         val groupChanges = GroupChanges(
             name = "NewGroupName",
@@ -278,8 +273,7 @@ class UpdateGroupFlowTest : GroupFlowTest() {
 
     @Test
     fun testGroupNameAndRemovedMembersModificationNonMd() = runTest {
-        val groupModel =
-            groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
+        val groupModel = groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
         assertNotNull(groupModel)
         val groupChanges = GroupChanges(
             name = "NewGroupName",
@@ -299,8 +293,7 @@ class UpdateGroupFlowTest : GroupFlowTest() {
 
     @Test
     fun testModificationOfDeletedGroupMd() = runTest {
-        val groupModel =
-            groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
+        val groupModel = groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
         assertNotNull(groupModel)
         val groupChanges = GroupChanges(
             name = "NewGroupName",
@@ -324,8 +317,7 @@ class UpdateGroupFlowTest : GroupFlowTest() {
 
     @Test
     fun testModificationOfDeletedGroupNonMd() = runTest {
-        val groupModel =
-            groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
+        val groupModel = groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
         assertNotNull(groupModel)
         val groupChanges = GroupChanges(
             name = "NewGroupName",
@@ -349,8 +341,7 @@ class UpdateGroupFlowTest : GroupFlowTest() {
 
     @Test
     fun testGroupNameAndAddedMembersModificationOfForeignGroupMd() = runTest {
-        val groupModel =
-            groupModelRepository.getByGroupIdentity(initialGroupModelData.groupIdentity)
+        val groupModel = groupModelRepository.getByGroupIdentity(initialGroupModelData.groupIdentity)
         assertNotNull(groupModel)
         val groupChanges = GroupChanges(
             name = "NewGroupName",
@@ -372,8 +363,7 @@ class UpdateGroupFlowTest : GroupFlowTest() {
 
     @Test
     fun testGroupNameAndAddedMembersModificationOfForeignGroupNonMd() = runTest {
-        val groupModel =
-            groupModelRepository.getByGroupIdentity(initialGroupModelData.groupIdentity)
+        val groupModel = groupModelRepository.getByGroupIdentity(initialGroupModelData.groupIdentity)
         assertNotNull(groupModel)
         val groupChanges = GroupChanges(
             name = "NewGroupName",
@@ -393,22 +383,48 @@ class UpdateGroupFlowTest : GroupFlowTest() {
         }
     }
 
+    @Test
+    fun shouldNotUpdateGroupWhenMdActiveButConnectionIsLost() = runTest {
+        // arrange
+        val groupModel = groupModelRepository.getByGroupIdentity(myInitialGroupModelData.groupIdentity)
+        val taskManager = ControlledTaskManager(emptyList())
+        val groupFlowDispatcher = getGroupFlowDispatcher(
+            setupConfig = SetupConfig.MULTI_DEVICE_ENABLED,
+            taskManager = taskManager,
+            connection = ConnectionDisconnected,
+        )
+        val groupChangesNewName = GroupChanges(
+            name = "NewGroupName",
+            profilePictureChange = null,
+            updatedMembers = myInitialGroupModelData.otherMembers,
+            groupModelData = myInitialGroupModelData,
+        )
+
+        // act
+        val groupFlowResult = groupFlowDispatcher
+            .runUpdateGroupFlow(groupChangesNewName, groupModel!!)
+            .await()
+
+        // assert
+        assertIs<GroupFlowResult.Failure.Network>(groupFlowResult)
+    }
+
     private suspend fun assertSuccessfulGroupUpdate(
         groupModel: GroupModel,
         groupChanges: GroupChanges,
         reflectionExpectation: ReflectionExpectation,
     ) {
-        assertTrue {
-            runGroupUpdate(
-                groupModel = groupModel,
-                groupChanges = groupChanges,
-                reflectionExpectation = reflectionExpectation,
-                successExpected = true,
-            )
-        }
-        val data = groupModel.data.value
-        assertNotNull(data)
-        data.assertChanges(groupChanges)
+        val groupFlowResult = runGroupUpdate(
+            groupModel = groupModel,
+            groupChanges = groupChanges,
+            reflectionExpectation = reflectionExpectation,
+            successExpected = true,
+        )
+        assertIs<GroupFlowResult.Success>(groupFlowResult)
+
+        val groupModelData = groupModel.data.value
+        assertNotNull(groupModelData)
+        groupModelData.assertChangesApplied(groupChanges)
     }
 
     private suspend fun assertUnsuccessfulGroupUpdate(
@@ -416,14 +432,13 @@ class UpdateGroupFlowTest : GroupFlowTest() {
         groupChanges: GroupChanges,
         reflectionExpectation: ReflectionExpectation,
     ) {
-        assertFalse {
-            runGroupUpdate(
-                groupModel = groupModel,
-                groupChanges = groupChanges,
-                reflectionExpectation = reflectionExpectation,
-                successExpected = false,
-            )
-        }
+        val groupFlowResult = runGroupUpdate(
+            groupModel = groupModel,
+            groupChanges = groupChanges,
+            reflectionExpectation = reflectionExpectation,
+            successExpected = false,
+        )
+        assertIs<GroupFlowResult.Failure>(groupFlowResult)
     }
 
     private suspend fun runGroupUpdate(
@@ -431,7 +446,7 @@ class UpdateGroupFlowTest : GroupFlowTest() {
         groupChanges: GroupChanges,
         reflectionExpectation: ReflectionExpectation,
         successExpected: Boolean,
-    ): Boolean {
+    ): GroupFlowResult {
         val groupModelData = groupModel.data.value
 
         // Prepare task manager and group flow dispatcher
@@ -444,11 +459,9 @@ class UpdateGroupFlowTest : GroupFlowTest() {
         )
 
         // Run update group flow
-        val result = groupFlowDispatcher.runUpdateGroupFlow(
-            null,
-            groupChanges,
-            groupModel,
-        ).await()
+        val groupFlowResult = groupFlowDispatcher
+            .runUpdateGroupFlow(groupChanges, groupModel)
+            .await()
 
         taskManager.pendingTaskAssertions.size.let { size ->
             if (size > 0) {
@@ -456,7 +469,7 @@ class UpdateGroupFlowTest : GroupFlowTest() {
             }
         }
 
-        return result
+        return groupFlowResult
     }
 
     private fun getExpectedTaskAssertions(
@@ -491,13 +504,12 @@ class UpdateGroupFlowTest : GroupFlowTest() {
     }
 
     /**
-     * Assert that the changes have been applied to the group model data.
+     * Assert that the [groupChanges] have been applied to the group model data.
      */
-    private fun GroupModelData.assertChanges(groupChanges: GroupChanges) {
+    private fun GroupModelData.assertChangesApplied(groupChanges: GroupChanges) {
         groupChanges.name?.let { newName ->
             assertEquals(newName, this.name)
         }
-
         assertContainsAll(this.otherMembers, groupChanges.addMembers)
         assertContainsNone(this.otherMembers, groupChanges.removeMembers)
     }

+ 14 - 14
app/src/androidTest/java/ch/threema/app/processors/IncomingMessageProcessorTest.kt

@@ -118,7 +118,7 @@ class IncomingMessageProcessorTest : MessageProcessorProvider() {
 
     @Test
     fun testIncomingDeliveryReceipt() = runTest {
-        val messageId = MessageId()
+        val messageId = MessageId.random()
 
         // Test 'received'
         assertSuccessfulMessageProcessing(
@@ -171,7 +171,7 @@ class IncomingMessageProcessorTest : MessageProcessorProvider() {
         assertSuccessfulMessageProcessing(
             DeliveryReceiptMessage().also {
                 it.receiptType = DELIVERYRECEIPT_MSGRECEIVED
-                it.receiptMessageIds = Array(100) { MessageId() }
+                it.receiptMessageIds = Array(100) { MessageId.random() }
                 it.messageId = MessageId(0)
             }.enrich(),
             contactA,
@@ -195,7 +195,7 @@ class IncomingMessageProcessorTest : MessageProcessorProvider() {
         val badMessage = TextMessage().also {
             it.fromIdentity = contactA.identity
             it.toIdentity = myContact.identity
-            it.messageId = MessageId()
+            it.messageId = MessageId.random()
             it.date = Date()
             it.text = "" // Bad message; cannot be decoded due to invalid length
         }
@@ -214,7 +214,7 @@ class IncomingMessageProcessorTest : MessageProcessorProvider() {
         val messageToB = TextMessage().also {
             it.fromIdentity = contactA.identity
             it.toIdentity = contactB.identity
-            it.messageId = MessageId()
+            it.messageId = MessageId.random()
             it.date = Date()
             it.text = "This message is for contact B!"
         }
@@ -233,18 +233,18 @@ class IncomingMessageProcessorTest : MessageProcessorProvider() {
         )
 
         val expectDeliveryReceiptSent = message.sendAutomaticDeliveryReceipt() &&
-            !message.hasFlags(ProtocolDefines.MESSAGE_FLAG_NO_DELIVERY_RECEIPTS)
+            !message.hasFlag(ProtocolDefines.MESSAGE_FLAG_NO_DELIVERY_RECEIPTS)
+
+        val sentMessage = sentMessagesNewTask.poll()
         if (expectDeliveryReceiptSent) {
-            val deliveryReceiptMessage = sentMessagesInsideTask.poll()
-            if (deliveryReceiptMessage is DeliveryReceiptMessage) {
-                assertContentEquals(
-                    messageId.messageId,
-                    deliveryReceiptMessage.receiptMessageIds[0].messageId,
-                )
-                assertEquals(DELIVERYRECEIPT_MSGRECEIVED, deliveryReceiptMessage.receiptType)
+            if (sentMessage is DeliveryReceiptMessage) {
+                assertContentEquals(messageId.messageId, sentMessage.receiptMessageIds[0].messageId)
+                assertEquals(DELIVERYRECEIPT_MSGRECEIVED, sentMessage.receiptType)
             } else {
-                fail("Instead of delivery receipt we got $deliveryReceiptMessage")
+                fail("Instead of delivery receipt we got $sentMessage")
             }
+        } else if (sentMessage != null) {
+            fail("Expected no message but got $sentMessage")
         }
 
         assertTrue(sentMessagesInsideTask.isEmpty())
@@ -267,7 +267,7 @@ class IncomingMessageProcessorTest : MessageProcessorProvider() {
     private fun AbstractMessage.enrich(): AbstractMessage {
         toIdentity = myContact.identity
         date = Date()
-        messageId = MessageId()
+        messageId = MessageId.random()
         return this
     }
 }

+ 14 - 13
app/src/androidTest/java/ch/threema/app/processors/MessageProcessorProvider.kt

@@ -37,6 +37,7 @@ import ch.threema.app.testutils.TestHelpers
 import ch.threema.app.testutils.TestHelpers.TestContact
 import ch.threema.app.testutils.TestHelpers.TestGroup
 import ch.threema.app.testutils.clearDatabaseAndCaches
+import ch.threema.app.utils.AppVersionProvider
 import ch.threema.app.utils.ConfigUtils
 import ch.threema.app.utils.ForwardSecurityStatusSender
 import ch.threema.base.crypto.HashedNonce
@@ -74,17 +75,17 @@ import ch.threema.domain.taskmanager.Task
 import ch.threema.domain.taskmanager.TaskCodec
 import ch.threema.domain.taskmanager.TaskManager
 import ch.threema.domain.taskmanager.toCspMessage
-import ch.threema.storage.DatabaseServiceNew
+import ch.threema.storage.DatabaseService
 import ch.threema.storage.models.ContactModel.AcquaintanceLevel
 import ch.threema.storage.models.GroupMemberModel
 import java.util.Queue
 import java.util.concurrent.ConcurrentLinkedQueue
 import junit.framework.TestCase.assertEquals
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.runBlocking
-import org.junit.After
-import org.junit.Before
 import org.junit.Rule
 import org.junit.rules.Timeout
 
@@ -293,7 +294,7 @@ open class MessageProcessorProvider {
     /**
      * Asserts that the correct identity is set up and fills the database with the initial data.
      */
-    @Before
+    @BeforeTest
     fun setup() {
         TestHelpers.setIdentity(
             ThreemaApplication.requireServiceManager(),
@@ -301,7 +302,7 @@ open class MessageProcessorProvider {
         )
 
         // Delete persisted tasks as they are not needed for tests
-        serviceManager.databaseServiceNew.taskArchiveFactory.deleteAll()
+        serviceManager.databaseService.taskArchiveFactory.deleteAll()
 
         // Replace original task manager (save a copy of it)
         originalTaskManager = serviceManager.taskManager
@@ -385,7 +386,7 @@ open class MessageProcessorProvider {
     /**
      * Set the original task manager again and wait until the connection has been started again.
      */
-    @After
+    @AfterTest
     fun cleanup() {
         if (this::originalTaskManager.isInitialized) {
             setTaskManager(originalTaskManager)
@@ -413,10 +414,10 @@ open class MessageProcessorProvider {
     private fun setTaskManager(taskManager: TaskManager) {
         val serviceManager = ThreemaApplication.requireServiceManager()
         val coreServiceManager = TestCoreServiceManager(
-            ThreemaApplication.getAppVersion(),
-            serviceManager.databaseServiceNew,
+            AppVersionProvider.appVersion,
+            serviceManager.databaseService,
             serviceManager.preferenceStore,
-            TaskArchiverImpl(serviceManager.databaseServiceNew.taskArchiveFactory),
+            TaskArchiverImpl(serviceManager.databaseService.taskArchiveFactory),
             serviceManager.deviceCookieManager,
             taskManager,
             serviceManager.multiDeviceManager as MultiDeviceManagerImpl,
@@ -455,7 +456,7 @@ open class MessageProcessorProvider {
      * database entries are needed.
      */
     open fun fillDatabase() {
-        val databaseService = serviceManager.databaseServiceNew
+        val databaseService = serviceManager.databaseService
         val contactStore = serviceManager.contactStore
         val fileService = serviceManager.fileService
 
@@ -473,7 +474,7 @@ open class MessageProcessorProvider {
 
     private fun addContactToDatabase(
         testContact: TestContact,
-        databaseService: DatabaseServiceNew,
+        databaseService: DatabaseService,
         contactStore: ContactStore,
         acquaintanceLevel: AcquaintanceLevel = AcquaintanceLevel.DIRECT,
     ) {
@@ -490,7 +491,7 @@ open class MessageProcessorProvider {
 
     private fun addGroupToDatabase(
         testGroup: TestGroup,
-        databaseService: DatabaseServiceNew,
+        databaseService: DatabaseService,
         fileService: FileService,
     ) {
         val groupModel = testGroup.groupModel
@@ -534,7 +535,7 @@ open class MessageProcessorProvider {
 
         // Assert that this message has been acked towards the server
         assertEquals(
-            message.hasFlags(ProtocolDefines.MESSAGE_FLAG_NO_SERVER_ACK),
+            message.hasFlag(ProtocolDefines.MESSAGE_FLAG_NO_SERVER_ACK),
             !localTaskCodec.ackedIncomingMessages.contains(message.messageId),
         )
 

+ 10 - 9
app/src/androidTest/java/ch/threema/app/protocol/IdentityBlockedStepsTest.kt

@@ -25,13 +25,14 @@ import ch.threema.app.DangerousTest
 import ch.threema.app.TestCoreServiceManager
 import ch.threema.app.TestTaskManager
 import ch.threema.app.ThreemaApplication
+import ch.threema.app.preference.service.PreferenceService
+import ch.threema.app.preference.service.PreferenceServiceImpl
 import ch.threema.app.services.BlockedIdentitiesService
 import ch.threema.app.services.GroupService
-import ch.threema.app.services.PreferenceService
-import ch.threema.app.services.PreferenceServiceImpl
 import ch.threema.app.testutils.TestHelpers
 import ch.threema.app.testutils.TestHelpers.TestContact
 import ch.threema.app.testutils.clearDatabaseAndCaches
+import ch.threema.app.utils.AppVersionProvider
 import ch.threema.data.TestDatabaseService
 import ch.threema.data.models.ContactModelData
 import ch.threema.data.repositories.ContactModelRepository
@@ -46,15 +47,15 @@ import ch.threema.domain.models.TypingIndicatorPolicy
 import ch.threema.domain.models.VerificationLevel
 import ch.threema.domain.models.WorkVerificationLevel
 import ch.threema.domain.stores.ContactStore
-import ch.threema.storage.DatabaseServiceNew
+import ch.threema.storage.DatabaseService
 import ch.threema.storage.models.ContactModel
 import ch.threema.storage.models.GroupMemberModel
 import ch.threema.storage.models.GroupModel
 import java.util.Date
+import kotlin.test.BeforeTest
+import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Test
 
 @DangerousTest
 class IdentityBlockedStepsTest {
@@ -74,7 +75,7 @@ class IdentityBlockedStepsTest {
     private val inNoGroup = TestContact("********")
     private val inLeftGroup = TestContact("--------")
 
-    @Before
+    @BeforeTest
     fun setup() {
         val serviceManager = ThreemaApplication.requireServiceManager()
 
@@ -84,7 +85,7 @@ class IdentityBlockedStepsTest {
 
         val databaseService = TestDatabaseService()
         val coreServiceManager = TestCoreServiceManager(
-            version = ThreemaApplication.getAppVersion(),
+            version = AppVersionProvider.appVersion,
             databaseService = databaseService,
             preferenceStore = serviceManager.preferenceStore,
             taskManager = TestTaskManager(UnusedTaskCodec()),
@@ -120,7 +121,7 @@ class IdentityBlockedStepsTest {
         }
 
         addKnownContacts()
-        addGroups(serviceManager.databaseServiceNew)
+        addGroups(serviceManager.databaseService)
     }
 
     @Test
@@ -341,7 +342,7 @@ class IdentityBlockedStepsTest {
         )
     }
 
-    private fun addGroups(databaseService: DatabaseServiceNew) = runBlocking {
+    private fun addGroups(databaseService: DatabaseService) = runBlocking {
         databaseService.groupModelFactory.apply {
             create(
                 GroupModel()

+ 0 - 340
app/src/androidTest/java/ch/threema/app/service/GroupInviteServiceTest.java

@@ -1,340 +0,0 @@
-/*  _____ _
- * |_   _| |_  _ _ ___ ___ _ __  __ _
- *   | | | ' \| '_/ -_) -_) '  \/ _` |_
- *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
- *
- * Threema for Android
- * Copyright (c) 2021-2025 Threema GmbH
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program 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 Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package ch.threema.app.service;
-
-import android.accounts.Account;
-import android.accounts.AccountManagerCallback;
-import android.net.Uri;
-
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.Date;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import ch.threema.app.BuildConfig;
-import ch.threema.app.ThreemaApplication;
-import ch.threema.app.exceptions.FileSystemNotPresentException;
-import ch.threema.app.services.ContactService;
-import ch.threema.app.services.GroupService;
-import ch.threema.app.services.UserService;
-import ch.threema.app.services.group.GroupInviteService;
-import ch.threema.app.services.group.GroupInviteServiceImpl;
-import ch.threema.app.services.license.LicenseService;
-import ch.threema.domain.protocol.csp.messages.group.GroupInviteData;
-import ch.threema.domain.protocol.csp.messages.group.GroupInviteToken;
-import ch.threema.domain.taskmanager.TriggerSource;
-import ch.threema.localcrypto.MasterKeyLockedException;
-import ch.threema.protobuf.url_payloads.GroupInvite;
-import ch.threema.storage.DatabaseServiceNew;
-import ch.threema.storage.models.group.GroupInviteModel;
-
-public class GroupInviteServiceTest {
-
-    private GroupService groupService;
-    private GroupInviteService groupInviteService;
-
-    static final String TEST_GROUP_NAME = "A nice little group";
-    static final String TEST_INVITE_NAME = "New unnamed link";
-    static String TEST_IDENTITY = "ECHOECHO";
-    static final GroupInvite.ConfirmationMode TEST_CONFIRMATION_MODE_AUTOMATIC = GroupInvite.ConfirmationMode.AUTOMATIC;
-    static GroupInviteToken TEST_TOKEN_VALID;
-    static GroupInviteModel TEST_INVITE_MODEL;
-    static String TEST_ENCODED_INVITE = "RUNIT0VDSE86MDAwMTAyMDMwNDA1MDYwNzA4MDkwYTBiMGMwZDBlMGY6QSBuaWNlIGxpdHRsZSBncm91cDow";
-
-    static {
-        try {
-            TEST_TOKEN_VALID = new GroupInviteToken(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
-            TEST_INVITE_MODEL = new GroupInviteModel.Builder()
-                .withGroupName(TEST_GROUP_NAME)
-                .withInviteName(TEST_INVITE_NAME)
-                .withToken(TEST_TOKEN_VALID)
-                .withManualConfirmation(false)
-                .build();
-        } catch (GroupInviteToken.InvalidGroupInviteTokenException |
-                 GroupInviteModel.MissingRequiredArgumentsException e) {
-            e.printStackTrace();
-        }
-    }
-
-    private final GroupInviteData TEST_INVITE_DATA = new GroupInviteData(
-        TEST_IDENTITY,
-        TEST_TOKEN_VALID,
-        TEST_GROUP_NAME,
-        TEST_CONFIRMATION_MODE_AUTOMATIC
-    );
-
-    @Before
-    public void setUp() {
-        // create new implementation while only implementing getIdentity with the TEST_IDENTITY because Powermock cannot be used in androidTest scope
-        UserService userService = new UserService() {
-            @Override
-            public void createIdentity(byte[] newRandomSeed) throws Exception {
-
-            }
-
-            @Override
-            public void removeIdentity() throws Exception {
-
-            }
-
-            @Nullable
-            @Override
-            public byte[] getUserProfilePicture() {
-                return null;
-            }
-
-            @Override
-            public boolean setUserProfilePicture(@NonNull File userProfilePicture, @NonNull TriggerSource triggerSource) {
-                return false;
-            }
-
-            @Override
-            public boolean setUserProfilePicture(@NonNull byte[] userProfilePicture, @NonNull TriggerSource triggerSource) {
-                return false;
-            }
-
-            @Override
-            public void removeUserProfilePicture(@NonNull TriggerSource triggerSource) {
-
-            }
-
-            @NonNull
-            @Override
-            public ContactService.ProfilePictureUploadData uploadUserProfilePictureOrGetPreviousUploadData() {
-                return null;
-            }
-
-            @Override
-            public Account getAccount() {
-                return null;
-            }
-
-            @Override
-            public Account getAccount(boolean createIfNotExists) {
-                return null;
-            }
-
-            @Override
-            public boolean checkAccount() {
-                return false;
-            }
-
-            @Override
-            public boolean enableAccountAutoSync(boolean enable) {
-                return false;
-            }
-
-            @Override
-            public void removeAccount() {
-
-            }
-
-            @Override
-            public boolean removeAccount(AccountManagerCallback<Boolean> callback) {
-                return false;
-            }
-
-            @Override
-            public boolean hasIdentity() {
-                return false;
-            }
-
-            @Override
-            public String getIdentity() {
-                return TEST_IDENTITY;
-            }
-
-            @Override
-            public boolean isMe(String identity) {
-                return false;
-            }
-
-            @Override
-            public byte[] getPublicKey() {
-                return new byte[0];
-            }
-
-            @Override
-            public byte[] getPrivateKey() {
-                return new byte[0];
-            }
-
-            @Override
-            public String getLinkedEmail() {
-                return null;
-            }
-
-            @Override
-            public String getLinkedMobileE164() {
-                return null;
-            }
-
-            @Override
-            public String getLinkedMobile() {
-                return null;
-            }
-
-            @Override
-            public String getLinkedMobile(boolean returnPendingNumber) {
-                return null;
-            }
-
-            @Override
-            public void linkWithEmail(String email, @NonNull TriggerSource triggerSource) throws Exception {
-
-            }
-
-            @Override
-            public void unlinkEmail(@NonNull TriggerSource triggerSource) throws Exception {
-
-            }
-
-            @Override
-            public int getEmailLinkingState() {
-                return 0;
-            }
-
-            @Override
-            public void checkEmailLinkState(@NonNull TriggerSource triggerSource) {
-
-            }
-
-            @Override
-            public Date linkWithMobileNumber(String number, @NonNull TriggerSource triggerSource) throws Exception {
-                return null;
-            }
-
-            @Override
-            public void makeMobileLinkCall() throws Exception {
-
-            }
-
-            @Override
-            public void unlinkMobileNumber(@NonNull TriggerSource triggerSource) throws Exception {
-
-            }
-
-            @Override
-            public boolean verifyMobileNumber(String code, @NonNull TriggerSource triggerSource) throws Exception {
-                return false;
-            }
-
-            @Override
-            public int getMobileLinkingState() {
-                return 0;
-            }
-
-            @Override
-            public long getMobileLinkingTime() {
-                return 0;
-            }
-
-            @Override
-            public String getPublicNickname() {
-                return null;
-            }
-
-            @Nullable
-            @Override
-            public String setPublicNickname(String publicNickname, @NonNull TriggerSource triggerSource) {
-                return null;
-            }
-
-            @Override
-            public boolean restoreIdentity(String backupString, String password) throws Exception {
-                return false;
-            }
-
-            @Override
-            public boolean restoreIdentity(String identity, byte[] privateKey, byte[] publicKey) throws Exception {
-                return false;
-            }
-
-            @Override
-            public void setPolicyResponse(String responseData, String signature, int policyErrorCode) {
-
-            }
-
-            @Override
-            public void setCredentials(LicenseService.Credentials credentials) {
-
-            }
-
-            @Override
-            public boolean sendFeatureMask() {
-                return false;
-            }
-
-            @Override
-            public boolean setRevocationKey(String revocationKey) {
-                return false;
-            }
-
-            @Override
-            public Date getLastRevocationKeySet() {
-                return null;
-            }
-
-            @Override
-            public void checkRevocationKey(boolean force) {
-
-            }
-
-            @Override
-            public void setForwardSecurityEnabled(boolean isFsEnabled) {
-
-            }
-        };
-        try {
-            this.groupService = ThreemaApplication.getServiceManager().getGroupService();
-        } catch (MasterKeyLockedException | FileSystemNotPresentException e) {
-            e.printStackTrace();
-        }
-        DatabaseServiceNew databaseServiceNew = ThreemaApplication.getServiceManager().getDatabaseServiceNew();
-        this.groupInviteService = new GroupInviteServiceImpl(userService, this.groupService, databaseServiceNew);
-    }
-
-    @Test
-    public void testEncodeDecodeGroupInvite() {
-        Uri encodedGroupInvite = groupInviteService.encodeGroupInviteLink(TEST_INVITE_MODEL);
-
-        Assert.assertEquals("https", encodedGroupInvite.getScheme());
-        Assert.assertEquals(BuildConfig.groupLinkActionUrl, encodedGroupInvite.getAuthority());
-        Assert.assertEquals("/join", encodedGroupInvite.getPath());
-        Assert.assertEquals(TEST_ENCODED_INVITE, encodedGroupInvite.getEncodedFragment());
-    }
-
-    @Test
-    public void testDecodeGroupInvite() throws IOException, GroupInviteToken.InvalidGroupInviteTokenException {
-        GroupInviteData inviteDataFromDecodedUri = groupInviteService.decodeGroupInviteLink(TEST_ENCODED_INVITE);
-
-        Assert.assertEquals(TEST_INVITE_DATA.getAdminIdentity(), inviteDataFromDecodedUri.getAdminIdentity());
-        Assert.assertEquals(TEST_INVITE_DATA.getToken(), inviteDataFromDecodedUri.getToken());
-        Assert.assertEquals(TEST_INVITE_DATA.getGroupName(), inviteDataFromDecodedUri.getGroupName());
-        Assert.assertEquals(TEST_INVITE_DATA.getConfirmationMode(), inviteDataFromDecodedUri.getConfirmationMode());
-    }
-}

+ 99 - 0
app/src/androidTest/java/ch/threema/app/service/GroupInviteServiceTest.kt

@@ -0,0 +1,99 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2021-2025 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.app.service
+
+import ch.threema.app.BuildConfig
+import ch.threema.app.ThreemaApplication
+import ch.threema.app.services.GroupService
+import ch.threema.app.services.UserService
+import ch.threema.app.services.group.GroupInviteService
+import ch.threema.app.services.group.GroupInviteServiceImpl
+import ch.threema.domain.protocol.csp.messages.group.GroupInviteData
+import ch.threema.domain.protocol.csp.messages.group.GroupInviteToken
+import ch.threema.protobuf.url_payloads.GroupInvite.ConfirmationMode
+import ch.threema.storage.models.group.GroupInviteModel
+import io.mockk.every
+import io.mockk.mockk
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class GroupInviteServiceTest {
+    private lateinit var groupService: GroupService
+    private lateinit var groupInviteService: GroupInviteService
+
+    @BeforeTest
+    fun setUp() {
+        val userService = mockk<UserService> {
+            every { identity } returns TEST_IDENTITY
+        }
+        groupService = ThreemaApplication.requireServiceManager().groupService
+        val databaseService = ThreemaApplication.requireServiceManager().databaseService
+        groupInviteService = GroupInviteServiceImpl(userService, groupService, databaseService)
+    }
+
+    @Test
+    fun testEncodeDecodeGroupInvite() {
+        val encodedGroupInvite = groupInviteService.encodeGroupInviteLink(testInviteModel)
+
+        assertEquals("https", encodedGroupInvite.scheme)
+        assertEquals(BuildConfig.groupLinkActionUrl, encodedGroupInvite.authority)
+        assertEquals("/join", encodedGroupInvite.path)
+        assertEquals(TEST_ENCODED_INVITE, encodedGroupInvite.encodedFragment)
+    }
+
+    @Test
+    fun testDecodeGroupInvite() {
+        val inviteDataFromDecodedUri = groupInviteService.decodeGroupInviteLink(TEST_ENCODED_INVITE)
+
+        assertEquals(testInviteData.adminIdentity, inviteDataFromDecodedUri.adminIdentity)
+        assertEquals(testInviteData.token, inviteDataFromDecodedUri.token)
+        assertEquals(testInviteData.groupName, inviteDataFromDecodedUri.groupName)
+        assertEquals(testInviteData.confirmationMode, inviteDataFromDecodedUri.confirmationMode)
+    }
+
+    private companion object {
+        const val TEST_GROUP_NAME = "A nice little group"
+        const val TEST_INVITE_NAME = "New unnamed link"
+        const val TEST_IDENTITY = "ECHOECHO"
+        const val TEST_ENCODED_INVITE = "RUNIT0VDSE86MDAwMTAyMDMwNDA1MDYwNzA4MDkwYTBiMGMwZDBlMGY6QSBuaWNlIGxpdHRsZSBncm91cDow"
+        val testTokenValid: GroupInviteToken
+        val testInviteModel: GroupInviteModel
+        val testInviteData: GroupInviteData
+
+        init {
+            testTokenValid = GroupInviteToken(ByteArray(16) { it.toByte() })
+            testInviteModel = GroupInviteModel.Builder()
+                .withGroupName(TEST_GROUP_NAME)
+                .withInviteName(TEST_INVITE_NAME)
+                .withToken(testTokenValid)
+                .withManualConfirmation(false)
+                .build()
+            testInviteData = GroupInviteData(
+                TEST_IDENTITY,
+                testTokenValid,
+                TEST_GROUP_NAME,
+                ConfirmationMode.AUTOMATIC,
+            )
+        }
+    }
+}

+ 11 - 9
app/src/androidTest/java/ch/threema/app/services/BlockedIdentitiesServiceTest.kt

@@ -27,6 +27,7 @@ import ch.threema.app.TestTaskManager
 import ch.threema.app.ThreemaApplication
 import ch.threema.app.listeners.ContactListener
 import ch.threema.app.managers.ListenerManager
+import ch.threema.app.preference.service.PreferenceServiceImpl
 import ch.threema.app.stores.PreferenceStore
 import ch.threema.base.crypto.NonceFactory
 import ch.threema.domain.helpers.ServerAckTaskCodec
@@ -43,16 +44,17 @@ class BlockedIdentitiesServiceTest {
 
     private val taskManager = TestTaskManager(ServerAckTaskCodec())
 
-    private val preferenceService = PreferenceServiceImpl(
-        ThreemaApplication.getAppContext(),
-        PreferenceStore(
+    private val preferenceService =
+        PreferenceServiceImpl(
             ThreemaApplication.getAppContext(),
-            ThreemaApplication.getMasterKey(),
-        ),
-        taskManager,
-        multiDeviceManager,
-        NonceFactory(TestNonceStore()),
-    )
+            PreferenceStore(
+                ThreemaApplication.getAppContext(),
+                ThreemaApplication.getMasterKey(),
+            ),
+            taskManager,
+            multiDeviceManager,
+            NonceFactory(TestNonceStore()),
+        )
 
     private val blockedIdentitiesService: BlockedIdentitiesService = BlockedIdentitiesServiceImpl(
         preferenceService,

+ 2 - 2
app/src/androidTest/java/ch/threema/app/tasks/GroupCreateTaskTest.kt

@@ -53,11 +53,11 @@ import ch.threema.storage.models.GroupModel
 import ch.threema.testhelpers.MUST_NOT_BE_CALLED
 import java.util.Date
 import javax.net.ssl.HttpsURLConnection
+import kotlin.test.BeforeTest
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertIs
 import kotlinx.coroutines.test.runTest
-import org.junit.Before
 
 @DangerousTest
 class GroupCreateTaskTest {
@@ -131,7 +131,7 @@ class GroupCreateTaskTest {
         )
     }
 
-    @Before
+    @BeforeTest
     fun setup() {
         clearDatabaseAndCaches(serviceManager)
 

+ 73 - 1
app/src/androidTest/java/ch/threema/app/tasks/PersistableTasksTest.kt

@@ -40,9 +40,9 @@ import java.util.Date
 import junit.framework.TestCase.assertEquals
 import junit.framework.TestCase.assertNotNull
 import junit.framework.TestCase.fail
+import kotlin.test.Test
 import kotlinx.coroutines.runBlocking
 import kotlinx.serialization.json.Json
-import org.junit.Test
 
 /**
  * These tests are useful to detect when a task cannot be created out of a persisted representation
@@ -531,6 +531,15 @@ class PersistableTasksTest {
         )
     }
 
+    @Test
+    fun testReflectContactSyncPolicyUpdate() {
+        assertValidEncoding(
+            ReflectSettingsSyncTask.ReflectContactSyncPolicySyncUpdate::class.java,
+            """{"type":"ch.threema.app.tasks.ReflectSettingsSyncTask.ReflectContactSyncPolicySyncUpdate.""" +
+                """ReflectContactSyncPolicySyncUpdateData"}""",
+        )
+    }
+
     @Test
     fun testReflectUnknownContactPolicyUpdate() {
         assertValidEncoding(
@@ -557,6 +566,60 @@ class PersistableTasksTest {
         )
     }
 
+    @Test
+    fun testReflectO2oCallPolicySyncUpdate() {
+        assertValidEncoding(
+            ReflectSettingsSyncTask.ReflectO2oCallPolicySyncUpdate::class.java,
+            """{"type":"ch.threema.app.tasks.ReflectSettingsSyncTask.ReflectO2oCallPolicySyncUpdate.""" +
+                """ReflectO2oCallPolicySyncUpdateData"}""",
+        )
+    }
+
+    @Test
+    fun testReflectO2oCallConnectionPolicySyncUpdate() {
+        assertValidEncoding(
+            ReflectSettingsSyncTask.ReflectO2oCallConnectionPolicySyncUpdate::class.java,
+            """{"type":"ch.threema.app.tasks.ReflectSettingsSyncTask.ReflectO2oCallConnectionPolicySyncUpdate.""" +
+                """ReflectO2oCallConnectionPolicySyncUpdateData"}""",
+        )
+    }
+
+    @Test
+    fun testReflectO2oCallVideoPolicySyncUpdate() {
+        assertValidEncoding(
+            ReflectSettingsSyncTask.ReflectO2oCallVideoPolicySyncUpdate::class.java,
+            """{"type":"ch.threema.app.tasks.ReflectSettingsSyncTask.ReflectO2oCallVideoPolicySyncUpdate.""" +
+                """ReflectO2oCallVideoPolicySyncUpdateData"}""",
+        )
+    }
+
+    @Test
+    fun testReflectGroupCallPolicySyncUpdate() {
+        assertValidEncoding(
+            ReflectSettingsSyncTask.ReflectGroupCallPolicySyncUpdate::class.java,
+            """{"type":"ch.threema.app.tasks.ReflectSettingsSyncTask.ReflectGroupCallPolicySyncUpdate.""" +
+                """ReflectGroupCallPolicySyncUpdateData"}""",
+        )
+    }
+
+    @Test
+    fun testReflectScreenshotPolicySyncUpdate() {
+        assertValidEncoding(
+            ReflectSettingsSyncTask.ReflectScreenshotPolicySyncUpdate::class.java,
+            """{"type":"ch.threema.app.tasks.ReflectSettingsSyncTask.ReflectScreenshotPolicySyncUpdate.""" +
+                """ReflectScreenshotPolicySyncUpdateData"}""",
+        )
+    }
+
+    @Test
+    fun testReflectKeyboardDataCollectionPolicySyncUpdate() {
+        assertValidEncoding(
+            ReflectSettingsSyncTask.ReflectKeyboardDataCollectionPolicySyncUpdate::class.java,
+            """{"type":"ch.threema.app.tasks.ReflectSettingsSyncTask.ReflectKeyboardDataCollectionPolicySyncUpdate.""" +
+                """ReflectKeyboardDataCollectionPolicySyncUpdateData"}""",
+        )
+    }
+
     @Test
     fun testReflectBlockedIdentitiesSyncUpdate() {
         assertValidEncoding(
@@ -565,6 +628,15 @@ class PersistableTasksTest {
         )
     }
 
+    @Test
+    fun testReflectExcludeFromSyncIdentitiesSyncUpdate() {
+        assertValidEncoding(
+            ReflectSettingsSyncTask.ReflectExcludeFromSyncIdentitiesSyncUpdate::class.java,
+            """{"type":"ch.threema.app.tasks.ReflectSettingsSyncTask.ReflectExcludeFromSyncIdentitiesSyncUpdate.""" +
+                """ReflectExcludeFromSyncIdentitiesSyncUpdateData"}""",
+        )
+    }
+
     @Test
     fun testGroupCreateTask() {
         assertValidEncoding(

+ 4 - 4
app/src/androidTest/java/ch/threema/app/testutils/AndroidTestUtils.kt

@@ -28,11 +28,11 @@ import ch.threema.data.models.GroupIdentity
 fun clearDatabaseAndCaches(serviceManager: ServiceManager) {
     // First get all available contacts and groups
     val contactIdentities =
-        serviceManager.databaseServiceNew.contactModelFactory.all.map { contact ->
+        serviceManager.databaseService.contactModelFactory.all.map { contact ->
             contact.identity
         }
     val groupModelRepository = serviceManager.modelRepositories.groups
-    val groups = serviceManager.databaseServiceNew.groupModelFactory.all
+    val groups = serviceManager.databaseService.groupModelFactory.all
         .map { group ->
             GroupIdentity(group.creatorIdentity, group.apiGroupId.toLong())
         }.onEach {
@@ -42,14 +42,14 @@ fun clearDatabaseAndCaches(serviceManager: ServiceManager) {
         }
 
     // Clear entire database
-    serviceManager.databaseServiceNew.writableDatabase.apply {
+    serviceManager.databaseService.writableDatabase.apply {
         rawExecSQL("PRAGMA writable_schema = 1;")
         rawExecSQL("DELETE FROM sqlite_master where type in ('table', 'index', 'trigger');")
         rawExecSQL("PRAGMA writable_schema = 0;")
         rawExecSQL("VACUUM;")
         rawExecSQL("PRAGMA integrity_check;")
         // Recreate the database
-        serviceManager.databaseServiceNew.onCreate(this)
+        serviceManager.databaseService.onCreate(this)
     }
 
     // Clear caches in services and trigger listeners to refresh the new models from database

+ 0 - 71
app/src/androidTest/java/ch/threema/app/testutils/CaptureLogcatOnTestFailureRule.java

@@ -1,71 +0,0 @@
-/*  _____ _
- * |_   _| |_  _ _ ___ ___ _ __  __ _
- *   | | | ' \| '_/ -_) -_) '  \/ _` |_
- *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
- *
- * Threema for Android
- * Copyright (c) 2021-2025 Threema GmbH
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program 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 Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package ch.threema.app.testutils;
-
-import org.junit.AssumptionViolatedException;
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-/**
- * Capture adb logcat on test failure.
- * <p>
- * Based on https://www.braze.com/resources/articles/logcat-junit-android-tests
- */
-public class CaptureLogcatOnTestFailureRule implements TestRule {
-    private static final String LOGCAT_HEADER = "\n================ Logcat Output ================\n";
-    private static final String STACKTRACE_HEADER = "\n================ Stacktrace ================\n";
-    private static final String ORIGINAL_CLASS_HEADER = "\nOriginal class: ";
-
-    @Override
-    public Statement apply(Statement base, Description description) {
-        return new Statement() {
-            @Override
-            public void evaluate() throws Throwable {
-                // Before test, clear logcat
-                TestHelpers.clearLogcat();
-
-                try {
-                    // Run statement
-                    base.evaluate();
-                } catch (Throwable originalThrowable) {
-                    if (originalThrowable instanceof AssumptionViolatedException) {
-                        throw originalThrowable;
-                    }
-
-                    // Fetch logcat logs
-                    final String testName = description.getMethodName() + "(" + description.getClassName() + ")";
-                    final String logcatLogs = TestHelpers.getTestLogs(testName);
-
-                    // Throw updated throwable
-                    final String thrownMessage = originalThrowable.getMessage()
-                        + ORIGINAL_CLASS_HEADER + originalThrowable.getClass().getName()
-                        + LOGCAT_HEADER + logcatLogs
-                        + STACKTRACE_HEADER;
-                    final Throwable modifiedThrowable = new Throwable(thrownMessage);
-                    modifiedThrowable.setStackTrace(originalThrowable.getStackTrace());
-                    throw modifiedThrowable;
-                }
-            }
-        };
-    }
-}

+ 0 - 55
app/src/androidTest/java/ch/threema/app/testutils/InstructionUtil.java

@@ -1,55 +0,0 @@
-/*  _____ _
- * |_   _| |_  _ _ ___ ___ _ __  __ _
- *   | | | ' \| '_/ -_) -_) '  \/ _` |_
- *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
- *
- * Threema for Android
- * Copyright (c) 2017-2025 Threema GmbH
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program 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 Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package ch.threema.app.testutils;
-
-import android.app.Activity;
-
-import com.azimolabs.conditionwatcher.Instruction;
-
-import androidx.test.InstrumentationRegistry;
-import ch.threema.app.TestApplication;
-
-public class InstructionUtil {
-    private InstructionUtil() {
-    }
-
-    public static Instruction waitForView(final int resourceId) {
-        return new Instruction() {
-            @Override
-            public String getDescription() {
-                return "wait for " + resourceId;
-            }
-
-            @Override
-            public boolean checkCondition() {
-                Activity activity = ((TestApplication)
-                    InstrumentationRegistry.getTargetContext().getApplicationContext()).getCurrentActivity();
-                if (activity == null) {
-                    return false;
-                }
-
-                return activity.findViewById(resourceId) != null;
-            }
-        };
-    }
-
-}

+ 1 - 63
app/src/androidTest/java/ch/threema/app/testutils/TestHelpers.java

@@ -28,9 +28,6 @@ import android.util.Log;
 
 import com.neilalexander.jnacl.NaCl;
 
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
 import java.util.Collection;
 import java.util.Date;
 import java.util.List;
@@ -97,7 +94,7 @@ public class TestHelpers {
 
         @NonNull
         public ContactModel getContactModel() {
-            return new ContactModel(this.identity, this.publicKey);
+            return ContactModel.create(this.identity, this.publicKey);
         }
 
         @NonNull
@@ -285,63 +282,4 @@ public class TestHelpers {
         Log.i(TAG, "Test identity restored: " + TEST_CONTACT.identity);
         return TEST_CONTACT.identity;
     }
-
-    public static void clearLogcat() {
-        try {
-            Runtime.getRuntime().exec(new String[]{"logcat", "-c"});
-        } catch (IOException e) {
-            Log.e(TAG, "Could not clear logcat", e);
-        }
-    }
-
-    /**
-     * Return adb logs since the start of the specified test.
-     * <p>
-     * Based on https://www.braze.com/resources/articles/logcat-junit-android-tests
-     */
-    public static String getTestLogs(@NonNull String testName) {
-        final StringBuilder logLines = new StringBuilder();
-
-        // Process id is used to filter messages
-        final String currentProcessId = Integer.toString(android.os.Process.myPid());
-
-        // A snippet of text that uniquely determines where the relevant logs start in the logcat
-        final String testStartMessage = "TestRunner: started: " + testName;
-
-        // When true, write every line from the logcat buffer to the string builder
-        boolean recording = false;
-
-        // Logcat command:
-        //   -d asks the command to completely dump to our buffer, then return
-        //   -v threadtime sets the output log format
-        final String[] command = new String[]{"logcat", "-d", "-v", "threadtime"};
-
-        BufferedReader bufferedReader = null;
-        try {
-            final Process process = Runtime.getRuntime().exec(command);
-            bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
-            String line;
-            while ((line = bufferedReader.readLine()) != null) {
-                if (line.contains(testStartMessage)) {
-                    recording = true;
-                }
-                if (recording) {
-                    logLines.append(line);
-                    logLines.append('\n');
-                }
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "Failed to run logcat command", e);
-        } finally {
-            if (bufferedReader != null) {
-                try {
-                    bufferedReader.close();
-                } catch (IOException e) {
-                    Log.e(TAG, "Failed to close buffered reader", e);
-                }
-            }
-        }
-
-        return logLines.toString();
-    }
 }

+ 1 - 1
app/src/androidTest/java/ch/threema/app/utils/BackgroundExecutorTest.kt

@@ -24,11 +24,11 @@ package ch.threema.app.utils
 import android.os.Looper
 import ch.threema.app.utils.executor.BackgroundExecutor
 import ch.threema.app.utils.executor.BackgroundTask
+import kotlin.test.Test
 import kotlin.test.assertFailsWith
 import kotlinx.coroutines.runBlocking
 import org.junit.Assert
 import org.junit.Rule
-import org.junit.Test
 import org.junit.rules.Timeout
 
 class BackgroundExecutorTest {

+ 7 - 7
app/src/androidTest/java/ch/threema/app/utils/BundledMessagesSendStepsTest.kt

@@ -30,15 +30,15 @@ import ch.threema.domain.protocol.csp.messages.fs.ForwardSecurityMode
 import ch.threema.domain.taskmanager.ActiveTask
 import ch.threema.domain.taskmanager.ActiveTaskCodec
 import java.util.Date
+import kotlin.test.BeforeTest
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertTrue
-import org.junit.Before
 
 class BundledMessagesSendStepsTest : MessageProcessorProvider() {
     private lateinit var outgoingCspMessageServices: OutgoingCspMessageServices
 
-    @Before
+    @BeforeTest
     fun initialize() {
         outgoingCspMessageServices = OutgoingCspMessageServices(
             serviceManager.forwardSecurityMessageProcessor,
@@ -58,7 +58,7 @@ class BundledMessagesSendStepsTest : MessageProcessorProvider() {
     @Test
     fun testContactMessage() {
         runInsideOfATask { handle ->
-            val messageId = MessageId()
+            val messageId = MessageId.random()
             val createdAt = Date()
             var hasBeenMarkedAsSent = false
             var forwardSecurityModes: Map<String, ForwardSecurityMode>? = null
@@ -95,7 +95,7 @@ class BundledMessagesSendStepsTest : MessageProcessorProvider() {
     @Test
     fun testGroupMessage() {
         runInsideOfATask { handle ->
-            val messageId = MessageId()
+            val messageId = MessageId.random()
             val createdAt = Date()
             val group = groupAB
             var hasBeenMarkedAsSent = false
@@ -138,7 +138,7 @@ class BundledMessagesSendStepsTest : MessageProcessorProvider() {
             OutgoingCspMessageHandle(
                 contactA.toBasicContact(),
                 OutgoingCspContactMessageCreator(
-                    MessageId(),
+                    MessageId.random(),
                     Date(),
                     contactA.identity,
                 ) {
@@ -149,7 +149,7 @@ class BundledMessagesSendStepsTest : MessageProcessorProvider() {
             OutgoingCspMessageHandle(
                 contactB.toBasicContact(),
                 OutgoingCspContactMessageCreator(
-                    MessageId(),
+                    MessageId.random(),
                     Date(),
                     contactA.identity,
                 ) {
@@ -160,7 +160,7 @@ class BundledMessagesSendStepsTest : MessageProcessorProvider() {
             OutgoingCspMessageHandle(
                 groupAB.members.map { it.toBasicContact() }.toSet(),
                 OutgoingCspGroupMessageCreator(
-                    MessageId(),
+                    MessageId.random(),
                     Date(),
                     groupAB.groupModel,
                 ) {

+ 0 - 60
app/src/androidTest/java/ch/threema/app/utils/GeoLocationUtilTest.kt

@@ -1,60 +0,0 @@
-/*  _____ _
- * |_   _| |_  _ _ ___ ___ _ __  __ _
- *   | | | ' \| '_/ -_) -_) '  \/ _` |_
- *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
- *
- * Threema for Android
- * Copyright (c) 2022-2025 Threema GmbH
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program 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 Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package ch.threema.app.utils
-
-import android.net.Uri
-import ch.threema.storage.models.data.LocationDataModel
-import org.junit.Assert.*
-import org.junit.Test
-
-class GeoLocationUtilTest {
-    private fun expectLocationData(expected: LocationDataModel?, uriStr: String) {
-        val uri = Uri.parse(uriStr)
-        val actual = GeoLocationUtil.getLocationDataFromGeoUri(uri)
-        if (expected == null) {
-            assertNull(actual)
-            return
-        }
-        assertNotNull(actual)
-        assertEquals(expected.latitude, actual?.latitude)
-        assertEquals(expected.longitude, actual?.longitude)
-        assertEquals(expected.poi, actual?.poi)
-        assertEquals(expected.accuracy, actual?.accuracy)
-    }
-
-    @Test
-    fun testGetLocationFromUri() {
-        val latLong1234 = LocationDataModel(
-            latitude = 12.0,
-            longitude = 34.0,
-            accuracy = 0.0,
-            poi = null,
-        )
-        expectLocationData(latLong1234, "geo:12,34;abcd=efg")
-        expectLocationData(latLong1234, "geo:12.0,34.00;a=b;c=d")
-        expectLocationData(latLong1234, "geo:12.0,34.0?q=12.0,34.0")
-        expectLocationData(latLong1234, "geo:1.0,2?q=12.0,34.0")
-        expectLocationData(latLong1234, "geo:0,0?q=12,34")
-        expectLocationData(latLong1234, "geo:12,34,56")
-        expectLocationData(latLong1234, "geo:12,34,56?z=12")
-    }
-}

+ 8 - 8
app/src/androidTest/java/ch/threema/app/utils/LinkifyUtilTest.kt

@@ -25,8 +25,8 @@ import android.text.Spanned
 import android.text.style.URLSpan
 import android.widget.TextView
 import androidx.test.platform.app.InstrumentationRegistry
+import kotlin.test.Test
 import org.junit.Assert.assertEquals
-import org.junit.Test
 
 class LinkifyUtilTest {
     /**
@@ -89,16 +89,16 @@ class LinkifyUtilTest {
 
     @Test
     fun testSimpleUrls() {
-        assertSingleSpan("www.threema.ch")
-        assertSingleSpan("a.b.c.d.e.f.threema.ch")
-        assertSingleSpan("https://www.threema.ch")
+        assertSingleSpan("www.threema.com")
+        assertSingleSpan("a.b.c.d.e.f.threema.com")
+        assertSingleSpan("https://www.threema.com")
     }
 
     @Test
     fun testInvalidUrls() {
-        assertNoSpan("www. threema .ch")
-        assertNoSpan("www.threema .ch")
-        assertNoSpan("www,threema,ch")
+        assertNoSpan("www. threema .com")
+        assertNoSpan("www.threema .com")
+        assertNoSpan("www,threema,com")
     }
 
     @Test
@@ -146,7 +146,7 @@ class LinkifyUtilTest {
     fun testMixedGeoUris() {
         assertSpans("geo:1,2 geo:1,2", setOf(0 to 7, 8 to 15))
         assertSpans("geo:1,2\ngeo:1,2", setOf(0 to 7, 8 to 15))
-        assertSpans("geo:1,2\nthreema.ch", setOf(0 to 7, 8 to 18))
+        assertSpans("geo:1,2\nthreema.com", setOf(0 to 7, 8 to 19))
     }
 
     @Test

+ 9 - 8
app/src/androidTest/java/ch/threema/app/voip/SdpTest.java

@@ -39,8 +39,8 @@ import java.util.concurrent.TimeUnit;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.MediumTest;
-import androidx.test.runner.AndroidJUnit4;
 import ch.threema.app.voip.util.SdpPatcher;
 
 import static junit.framework.Assert.assertEquals;
@@ -160,18 +160,17 @@ public class SdpTest {
                 "a=extmap:10 urn:ietf:params:rtp-hdrext:encrypt urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n" +
                 "a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n" +
                 "a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n" +
+                "a=extmap:15 urn:ietf:params:rtp-hdrext:encrypt http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n" +
                 "a=extmap:16 urn:ietf:params:rtp-hdrext:encrypt http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n" +
                 "a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n" +
                 "a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\n" +
                 "a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n" +
                 "a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n" +
-                "a=extmap:15 urn:ietf:params:rtp-hdrext:encrypt http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n" +
                 "a=extmap:17 urn:ietf:params:rtp-hdrext:encrypt urn:ietf:params:rtp-hdrext:sdes:mid\r\n" +
-                "a=extmap:18 urn:ietf:params:rtp-hdrext:encrypt urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n" +
-                "a=extmap:19 urn:ietf:params:rtp-hdrext:encrypt urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n" +
                 "a=sendrecv\r\n" +
                 "a=msid:3MACALL 3MACALLa0\r\n" +
                 "a=rtcp-mux\r\n" +
+                "a=rtcp-rsize\r\n" +
                 "a=rtpmap:111 opus/48000/2\r\n" +
                 "a=rtcp-fb:111 transport-cc\r\n" +
                 "a=fmtp:111 minptime=10;useinbandfec=1\r\n" +
@@ -202,14 +201,12 @@ public class SdpTest {
                 "a=setup:actpass\r\n" +
                 "a=mid:1\r\n" +
                 "a=extmap:25 urn:ietf:params:rtp-hdrext:encrypt http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07\r\n" +
-                "a=extmap:26 urn:ietf:params:rtp-hdrext:encrypt http://www.webrtc.org/experiments/rtp-hdrext/color-space\r\n" +
                 "a=extmap:14 urn:ietf:params:rtp-hdrext:toffset\r\n" +
                 "a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n" +
                 "a=extmap:13 urn:3gpp:video-orientation\r\n" +
                 "a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n" +
                 "a=extmap:12 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\n" +
                 "a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\r\n" +
-                "a=extmap:17 urn:ietf:params:rtp-hdrext:encrypt urn:ietf:params:rtp-hdrext:sdes:mid\r\n" +
                 "a=extmap:8 http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07\r\n" +
                 "a=extmap:9 http://www.webrtc.org/experiments/rtp-hdrext/color-space\r\n" +
                 "a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\n" +
@@ -222,7 +219,9 @@ public class SdpTest {
                 "a=extmap:22 urn:ietf:params:rtp-hdrext:encrypt http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\n" +
                 "a=extmap:23 urn:ietf:params:rtp-hdrext:encrypt http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\r\n" +
                 "a=extmap:24 urn:ietf:params:rtp-hdrext:encrypt http://www.webrtc.org/experiments/rtp-hdrext/video-timing\r\n" +
+                "a=extmap:26 urn:ietf:params:rtp-hdrext:encrypt http://www.webrtc.org/experiments/rtp-hdrext/color-space\r\n" +
                 "a=extmap:11 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\r\n" +
+                "a=extmap:17 urn:ietf:params:rtp-hdrext:encrypt urn:ietf:params:rtp-hdrext:sdes:mid\r\n" +
                 "a=extmap:18 urn:ietf:params:rtp-hdrext:encrypt urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n" +
                 "a=extmap:19 urn:ietf:params:rtp-hdrext:encrypt urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n" +
                 "a=sendrecv\r\n" +
@@ -323,6 +322,7 @@ public class SdpTest {
                 "a=sendrecv\r\n" +
                 "a=msid:3MACALL 3MACALLa0\r\n" +
                 "a=rtcp-mux\r\n" +
+                "a=rtcp-rsize\r\n" +
                 "a=rtpmap:111 opus/48000/2\r\n" +
                 "a=rtcp-fb:111 transport-cc\r\n" +
                 "a=fmtp:111 minptime=10;useinbandfec=1\r\n" +
@@ -386,6 +386,7 @@ public class SdpTest {
         matches.add("^a=sendrecv$");
         matches.add("^a=msid:3MACALL 3MACALLa0");
         matches.add("^a=rtcp-mux$");
+        matches.add("^a=rtcp-rsize$");
         matches.add("^a=rtpmap:\\d+ opus/48000/2$");
         matches.add("^a=rtcp-fb:\\d+ transport-cc$");
         matches.add("^a=fmtp:\\d+ minptime=10;useinbandfec=1;stereo=0;sprop-stereo=0;cbr=1$");
@@ -558,7 +559,7 @@ public class SdpTest {
 
         // Compare SDP
         assertNotNull(events.localSdp);
-        assertEquals(events.localSdp.type, SessionDescription.Type.OFFER);
+        assertEquals(SessionDescription.Type.OFFER, events.localSdp.type);
         this.validateDescription(events.localSdp, videoEnabled, true);
     }
 
@@ -612,7 +613,7 @@ public class SdpTest {
 
         // Compare SDP
         assertNotNull(events.localSdp);
-        assertEquals(events.localSdp.type, SessionDescription.Type.ANSWER);
+        assertEquals(SessionDescription.Type.ANSWER, events.localSdp.type);
         this.validateDescription(events.localSdp, videoEnabled, false);
     }
 

+ 4 - 4
app/src/androidTest/java/ch/threema/app/webclient/activities/SessionsActivityTest.java

@@ -47,7 +47,7 @@ import ch.threema.app.DangerousTest;
 import ch.threema.app.R;
 import ch.threema.app.ScreenshotTakingRule;
 import ch.threema.app.ThreemaApplication;
-import ch.threema.storage.DatabaseServiceNew;
+import ch.threema.storage.DatabaseService;
 import ch.threema.storage.models.WebClientSessionModel;
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
@@ -102,9 +102,9 @@ public class SessionsActivityTest {
      * Clear all sessions.
      */
     private static void clearSessions() {
-        final DatabaseServiceNew databaseService = ThreemaApplication
+        final DatabaseService databaseService = ThreemaApplication
             .getServiceManager()
-            .getDatabaseServiceNew();
+            .getDatabaseService();
         databaseService.getWebClientSessionModelFactory().deleteAll();
     }
 
@@ -119,7 +119,7 @@ public class SessionsActivityTest {
         @NonNull Date lastConnection,
         @NonNull Browser browser
     ) {
-        final DatabaseServiceNew databaseService = Objects.requireNonNull(ThreemaApplication.getServiceManager()).getDatabaseServiceNew();
+        final DatabaseService databaseService = Objects.requireNonNull(ThreemaApplication.getServiceManager()).getDatabaseService();
         final WebClientSessionModel model = new WebClientSessionModel();
 
         model.setLabel(label);

+ 5 - 7
app/src/androidTest/java/ch/threema/data/TestDatabaseService.kt

@@ -22,15 +22,13 @@
 package ch.threema.data
 
 import androidx.test.core.app.ApplicationProvider
-import ch.threema.app.services.UpdateSystemServiceImpl
-import ch.threema.storage.DatabaseServiceNew
+import ch.threema.storage.DatabaseService
 
 /**
  * An in-memory database used in android tests.
  */
-class TestDatabaseService : DatabaseServiceNew(
-    ApplicationProvider.getApplicationContext(),
-    null,
-    "test-database-key",
-    UpdateSystemServiceImpl(),
+class TestDatabaseService : DatabaseService(
+    context = ApplicationProvider.getApplicationContext(),
+    databaseName = null,
+    databaseKey = "test-database-key",
 )

+ 8 - 7
app/src/androidTest/java/ch/threema/data/repositories/ContactModelRepositoryTest.kt

@@ -26,6 +26,7 @@ import ch.threema.app.TestMultiDeviceManager
 import ch.threema.app.TestTaskManager
 import ch.threema.app.ThreemaApplication
 import ch.threema.app.testutils.TestHelpers
+import ch.threema.app.utils.AppVersionProvider
 import ch.threema.data.TestDatabaseService
 import ch.threema.data.models.ContactModelData
 import ch.threema.data.models.ContactModelData.Companion.getIdColorIndex
@@ -45,6 +46,7 @@ import ch.threema.testhelpers.randomIdentity
 import com.neilalexander.jnacl.NaCl
 import java.util.Date
 import junit.framework.TestCase.assertNotNull
+import kotlin.test.BeforeTest
 import kotlin.test.Test
 import kotlin.test.assertContentEquals
 import kotlin.test.assertEquals
@@ -53,7 +55,6 @@ import kotlin.test.assertNull
 import kotlin.test.assertTrue
 import kotlin.test.fail
 import kotlinx.coroutines.runBlocking
-import org.junit.Before
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
@@ -138,7 +139,7 @@ class ContactModelRepositoryTest(private val contactModelData: ContactModelData)
         )
     }
 
-    @Before
+    @BeforeTest
     fun before() {
         TestHelpers.setIdentity(
             ThreemaApplication.requireServiceManager(),
@@ -148,7 +149,7 @@ class ContactModelRepositoryTest(private val contactModelData: ContactModelData)
         // Instantiate services where MD is disabled
         this.databaseService = TestDatabaseService()
         this.coreServiceManager = TestCoreServiceManager(
-            version = ThreemaApplication.getAppVersion(),
+            version = AppVersionProvider.appVersion,
             databaseService = databaseService,
             preferenceStore = ThreemaApplication.requireServiceManager().preferenceStore,
             taskManager = TestTaskManager(UnusedTaskCodec()),
@@ -159,7 +160,7 @@ class ContactModelRepositoryTest(private val contactModelData: ContactModelData)
         this.databaseServiceMd = TestDatabaseService()
         this.taskCodecMd = TransactionAckTaskCodec()
         this.coreServiceManagerMd = TestCoreServiceManager(
-            version = ThreemaApplication.getAppVersion(),
+            version = AppVersionProvider.appVersion,
             databaseService = databaseServiceMd,
             preferenceStore = ThreemaApplication.requireServiceManager().preferenceStore,
             multiDeviceManager = TestMultiDeviceManager(
@@ -236,8 +237,8 @@ class ContactModelRepositoryTest(private val contactModelData: ContactModelData)
         val publicKey = nonSecureRandomArray(32)
 
         // Create contact using "old model"
-        databaseService.contactModelFactory.createOrUpdate(ContactModel(identity, publicKey))
-        databaseServiceMd.contactModelFactory.createOrUpdate(ContactModel(identity, publicKey))
+        databaseService.contactModelFactory.createOrUpdate(ContactModel.create(identity, publicKey))
+        databaseServiceMd.contactModelFactory.createOrUpdate(ContactModel.create(identity, publicKey))
 
         // Fetch contact using "new model"
         val model = contactModelRepository.getByIdentity(identity)!!
@@ -420,7 +421,7 @@ class ContactModelRepositoryTest(private val contactModelData: ContactModelData)
         // Create contact using "old model"
         val identity = randomIdentity()
         databaseService.contactModelFactory.createOrUpdate(
-            ContactModel(
+            ContactModel.create(
                 identity,
                 nonSecureRandomArray(32),
             ),

+ 5 - 4
app/src/androidTest/java/ch/threema/data/repositories/EditHistoryRepositoryTest.kt

@@ -25,6 +25,7 @@ import ch.threema.app.TestCoreServiceManager
 import ch.threema.app.TestTaskManager
 import ch.threema.app.ThreemaApplication
 import ch.threema.app.testutils.TestHelpers
+import ch.threema.app.utils.AppVersionProvider
 import ch.threema.data.TestDatabaseService
 import ch.threema.data.storage.EditHistoryDao
 import ch.threema.data.storage.EditHistoryDaoImpl
@@ -34,17 +35,17 @@ import ch.threema.storage.models.GroupMessageModel
 import ch.threema.storage.models.MessageModel
 import ch.threema.storage.models.MessageType
 import java.util.UUID
+import kotlin.test.BeforeTest
+import kotlin.test.Test
 import kotlin.test.assertFailsWith
 import org.junit.Assert
-import org.junit.Before
-import org.junit.Test
 
 class EditHistoryRepositoryTest {
     private lateinit var databaseService: TestDatabaseService
     private lateinit var editHistoryRepository: EditHistoryRepository
     private lateinit var editHistoryDao: EditHistoryDao
 
-    @Before
+    @BeforeTest
     fun before() {
         TestHelpers.setIdentity(
             ThreemaApplication.requireServiceManager(),
@@ -53,7 +54,7 @@ class EditHistoryRepositoryTest {
 
         databaseService = TestDatabaseService()
         val testCoreServiceManager = TestCoreServiceManager(
-            version = ThreemaApplication.getAppVersion(),
+            version = AppVersionProvider.appVersion,
             databaseService = databaseService,
             preferenceStore = ThreemaApplication.requireServiceManager().preferenceStore,
             taskManager = TestTaskManager(UnusedTaskCodec()),

+ 5 - 4
app/src/androidTest/java/ch/threema/data/repositories/EmojiReactionsRepositoryTest.kt

@@ -24,6 +24,7 @@ package ch.threema.data.repositories
 import ch.threema.app.TestCoreServiceManager
 import ch.threema.app.TestTaskManager
 import ch.threema.app.ThreemaApplication
+import ch.threema.app.utils.AppVersionProvider
 import ch.threema.data.ModelTypeCache
 import ch.threema.data.TestDatabaseService
 import ch.threema.data.models.EmojiReactionData
@@ -39,14 +40,14 @@ import ch.threema.storage.models.MessageModel
 import ch.threema.storage.models.MessageType
 import java.util.Date
 import java.util.UUID
+import kotlin.test.BeforeTest
+import kotlin.test.Test
 import kotlin.test.assertContentEquals
 import kotlin.test.assertFailsWith
 import kotlin.test.assertNotNull
 import kotlin.test.assertNull
 import kotlin.test.fail
 import org.junit.Assert
-import org.junit.Before
-import org.junit.Test
 
 class EmojiReactionsRepositoryTest {
     private lateinit var testCoreServiceManager: TestCoreServiceManager
@@ -54,11 +55,11 @@ class EmojiReactionsRepositoryTest {
     private lateinit var emojiReactionsRepository: EmojiReactionsRepository
     private lateinit var emojiReactionDao: EmojiReactionsDao
 
-    @Before
+    @BeforeTest
     fun before() {
         databaseService = TestDatabaseService()
         testCoreServiceManager = TestCoreServiceManager(
-            version = ThreemaApplication.getAppVersion(),
+            version = AppVersionProvider.appVersion,
             databaseService = databaseService,
             preferenceStore = ThreemaApplication.requireServiceManager().preferenceStore,
             taskManager = TestTaskManager(UnusedTaskCodec()),

+ 5 - 4
app/src/androidTest/java/ch/threema/data/repositories/GroupModelRepositoryTest.kt

@@ -25,6 +25,7 @@ import ch.threema.app.TestCoreServiceManager
 import ch.threema.app.TestTaskManager
 import ch.threema.app.ThreemaApplication
 import ch.threema.app.testutils.TestHelpers
+import ch.threema.app.utils.AppVersionProvider
 import ch.threema.data.TestDatabaseService
 import ch.threema.data.models.GroupIdentity
 import ch.threema.data.models.GroupModelDataFactory
@@ -35,11 +36,11 @@ import ch.threema.domain.helpers.UnusedTaskCodec
 import ch.threema.domain.models.GroupId
 import ch.threema.storage.models.GroupModel
 import java.util.Date
+import kotlin.test.BeforeTest
+import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
 import kotlin.test.assertNull
-import org.junit.Before
-import org.junit.Test
 
 class GroupModelRepositoryTest {
     private lateinit var databaseService: TestDatabaseService
@@ -65,7 +66,7 @@ class GroupModelRepositoryTest {
         )
     }
 
-    @Before
+    @BeforeTest
     fun before() {
         TestHelpers.setIdentity(
             ThreemaApplication.requireServiceManager(),
@@ -75,7 +76,7 @@ class GroupModelRepositoryTest {
         this.databaseService = TestDatabaseService()
         this.databaseBackend = SqliteDatabaseBackend(databaseService)
         this.coreServiceManager = TestCoreServiceManager(
-            version = ThreemaApplication.getAppVersion(),
+            version = AppVersionProvider.appVersion,
             databaseService = databaseService,
             preferenceStore = ThreemaApplication.requireServiceManager().preferenceStore,
             taskManager = TestTaskManager(UnusedTaskCodec()),

+ 20 - 18
app/src/androidTest/java/ch/threema/app/services/systemupdate/SystemUpdateHelpersTest.kt → app/src/androidTest/java/ch/threema/storage/DatabaseExtensionsTest.kt

@@ -19,39 +19,41 @@
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
  */
 
-package ch.threema.app.services.systemupdate
+package ch.threema.storage
 
+import kotlin.test.BeforeTest
+import kotlin.test.Test
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue
 import net.zetetic.database.sqlcipher.SQLiteDatabase
-import org.junit.Before
-import org.junit.Test
 
-class SystemUpdateHelpersTest {
+class DatabaseExtensionsTest {
     private var inMemoryDatabase: SQLiteDatabase = SQLiteDatabase.create(null)
 
-    @Before
+    @BeforeTest
     fun setUp() {
-        this.inMemoryDatabase.execSQL("CREATE TABLE IF NOT EXISTS testtable (hello TEXT, world INTEGER)")
+        inMemoryDatabase.execSQL("CREATE TABLE IF NOT EXISTS testtable (hello TEXT, world INTEGER)")
+    }
+
+    @Test
+    fun testTableExistsForNonExistingTable() {
+        assertFalse(inMemoryDatabase.tableExists("non_existing_table"))
+    }
+
+    @Test
+    fun testTableExistsForExistingTable() {
+        assertTrue(inMemoryDatabase.tableExists("testtable"))
     }
 
     @Test
     fun testFieldExistNonExistingTable() {
-        assertFalse {
-            fieldExists(this.inMemoryDatabase, "non_existing_table", "non_existing_field")
-        }
+        assertFalse(inMemoryDatabase.fieldExists("non_existing_table", "non_existing_field"))
     }
 
     @Test
     fun testFieldExistExistingTable() {
-        assertFalse {
-            fieldExists(this.inMemoryDatabase, "testtable", "non_existing_field")
-        }
-        assertTrue {
-            fieldExists(this.inMemoryDatabase, "testtable", "hello")
-        }
-        assertTrue {
-            fieldExists(this.inMemoryDatabase, "testtable", "world")
-        }
+        assertFalse(inMemoryDatabase.fieldExists("testtable", "non_existing_field"))
+        assertTrue(inMemoryDatabase.fieldExists("testtable", "hello"))
+        assertTrue(inMemoryDatabase.fieldExists("testtable", "world"))
     }
 }

+ 5 - 5
app/src/androidTest/java/ch/threema/storage/DatabaseNonceStoreTest.kt

@@ -30,10 +30,10 @@ import ch.threema.base.crypto.NonceStore
 import ch.threema.domain.stores.IdentityStoreInterface
 import javax.crypto.Mac
 import javax.crypto.spec.SecretKeySpec
-import org.junit.After
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
 import org.junit.Assert.*
-import org.junit.Before
-import org.junit.Test
 
 class DatabaseNonceStoreTest {
     private lateinit var tempDbFileName: String
@@ -42,7 +42,7 @@ class DatabaseNonceStoreTest {
     private val store: NonceStore
         get() = _store
 
-    @Before
+    @BeforeTest
     fun setup() {
         tempDbFileName = "threema-nonce-test-${System.currentTimeMillis()}.db"
         val identityStore = TestIdentityStore()
@@ -53,7 +53,7 @@ class DatabaseNonceStoreTest {
         )
     }
 
-    @After
+    @AfterTest
     fun teardown() {
         _store.close()
         ApplicationProvider

+ 1 - 3
app/src/androidTest/java/ch/threema/storage/SQLDHSessionStoreTest.java

@@ -33,7 +33,6 @@ import java.util.ArrayList;
 import java.util.List;
 
 import androidx.test.core.app.ApplicationProvider;
-import ch.threema.app.ThreemaApplication;
 import ch.threema.domain.fs.DHSession;
 import ch.threema.domain.fs.DHSessionId;
 import ch.threema.domain.helpers.DummyUsers;
@@ -59,8 +58,7 @@ public class SQLDHSessionStoreTest {
         store = new SQLDHSessionStore(
             ApplicationProvider.getApplicationContext(),
             DATABASE_KEY,
-            tempDbFileName,
-            ThreemaApplication.requireServiceManager().getUpdateSystemService()
+            tempDbFileName
         );
     }
 

+ 6 - 6
app/src/androidTest/java/ch/threema/storage/TaskArchiveFactoryTest.kt

@@ -24,21 +24,21 @@ package ch.threema.storage
 import ch.threema.app.ThreemaApplication
 import ch.threema.storage.factories.TaskArchiveFactory
 import junit.framework.TestCase.assertEquals
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
 
 class TaskArchiveFactoryTest {
     private lateinit var taskArchiveFactory: TaskArchiveFactory
 
-    @Before
+    @BeforeTest
     fun setup() {
         taskArchiveFactory =
-            ThreemaApplication.requireServiceManager().databaseServiceNew.taskArchiveFactory
+            ThreemaApplication.requireServiceManager().databaseService.taskArchiveFactory
         taskArchiveFactory.deleteAll()
     }
 
-    @After
+    @AfterTest
     fun tearDown() {
         taskArchiveFactory.deleteAll()
     }

+ 0 - 72
app/src/androidTest/java/com/azimolabs/conditionwatcher/ConditionWatcher.java

@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2016 Azimo
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.azimolabs.conditionwatcher;
-
-public class ConditionWatcher {
-
-    public static final int CONDITION_NOT_MET = 0;
-    public static final int CONDITION_MET = 1;
-    public static final int TIMEOUT = 2;
-
-    public static final int DEFAULT_TIMEOUT_LIMIT = 1000 * 60;
-    public static final int DEFAULT_INTERVAL = 250;
-
-    private int timeoutLimit = DEFAULT_TIMEOUT_LIMIT;
-    private int watchInterval = DEFAULT_INTERVAL;
-
-    private static ConditionWatcher conditionWatcher;
-
-    private ConditionWatcher() {
-        super();
-    }
-
-    public static ConditionWatcher getInstance() {
-        if (conditionWatcher == null) {
-            conditionWatcher = new ConditionWatcher();
-        }
-        return conditionWatcher;
-    }
-
-    public static void waitForCondition(Instruction instruction) throws Exception {
-        int status = CONDITION_NOT_MET;
-        int elapsedTime = 0;
-
-        do {
-            if (instruction.checkCondition()) {
-                status = CONDITION_MET;
-            } else {
-                elapsedTime += getInstance().watchInterval;
-                Thread.sleep(getInstance().watchInterval);
-            }
-
-            if (elapsedTime == getInstance().timeoutLimit) {
-                status = TIMEOUT;
-                break;
-            }
-        } while (status != CONDITION_MET);
-
-        if (status == TIMEOUT)
-            throw new Exception(instruction.getDescription() + " - took more than " + getInstance().timeoutLimit / 1000 + " seconds. Test stopped.");
-    }
-
-    public static void setWatchInterval(int watchInterval) {
-        getInstance().watchInterval = watchInterval;
-    }
-
-    public static void setTimeoutLimit(int ms) {
-        getInstance().timeoutLimit = ms;
-    }
-}

+ 0 - 35
app/src/androidTest/java/com/azimolabs/conditionwatcher/Instruction.java

@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2016 Azimo
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.azimolabs.conditionwatcher;
-
-import android.os.Bundle;
-
-public abstract class Instruction {
-
-    private Bundle dataContainer = new Bundle();
-
-    public final void setData(Bundle dataContainer) {
-        this.dataContainer = dataContainer;
-    }
-
-    public final Bundle getDataContainer() {
-        return dataContainer;
-    }
-
-    public abstract String getDescription();
-
-    public abstract boolean checkCondition();
-}

BIN=BIN
app/src/blue/ic_launcher-playstore.png


BIN=BIN
app/src/blue/ic_launcher-web.png


+ 17 - 11
app/src/main/java/ch/threema/app/compose/theme/color/CustomColor.kt → app/src/blue/java/ch/threema/app/compose/theme/color/BrandColor.kt

@@ -4,7 +4,7 @@
  *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
  *
  * Threema for Android
- * Copyright (c) 2024-2025 Threema GmbH
+ * Copyright (c) 2025 Threema GmbH
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License, version 3,
@@ -21,13 +21,19 @@
 
 package ch.threema.app.compose.theme.color
 
-import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.staticCompositionLocalOf
-import androidx.compose.ui.graphics.Color
-
-@Immutable
-data class CustomColor(
-    val messageBubbleContainerReceive: Color = Color.Red,
-)
-
-val LocalCustomColor = staticCompositionLocalOf { CustomColor() }
+/**
+ *  Colors for `Work` flavor
+ */
+@Suppress("unused")
+object BrandColor {
+    const val SHADE_50 = 0xFFDCF5FF
+    const val SHADE_100 = 0xFFC0EDFF
+    const val SHADE_200 = 0xFFA4E6FF
+    const val SHADE_300 = 0xFF89DEFF
+    const val SHADE_400 = 0xFF6DD7FF
+    const val SHADE_500 = 0xFF51CFFF
+    const val SHADE_600 = 0xFF41AFE0
+    const val SHADE_700 = 0xFF318FC1
+    const val SHADE_800 = 0xFF002E63
+    const val SHADE_900 = 0xFF001C3B
+}

+ 0 - 60
app/src/blue/java/ch/threema/app/compose/theme/color/ColorsDark.kt

@@ -1,60 +0,0 @@
-/*  _____ _
- * |_   _| |_  _ _ ___ ___ _ __  __ _
- *   | | | ' \| '_/ -_) -_) '  \/ _` |_
- *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
- *
- * Threema for Android
- * Copyright (c) 2024-2025 Threema GmbH
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program 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 Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package ch.threema.app.compose.theme.color
-
-import androidx.compose.ui.graphics.Color
-
-object ColorsDark : ComposeColorPaletteDark() {
-    override val primary = Color(0xFFA4C8FF)
-    override val onPrimary = Color(0xFF00315E)
-    override val primaryContainer = Color(0xFF004784)
-    override val onPrimaryContainer = Color(0xFFD4E3FF)
-    override val secondary = Color(0xFFBCC7DC)
-    override val onSecondary = Color(0xFF263141)
-    override val secondaryContainer = Color(0xFF3D434E)
-    override val onSecondaryContainer = Color(0xFFDAE1EF)
-    override val tertiary = Color(0xFFD9BDE2)
-    override val onTertiary = Color(0xFF3D2946)
-    override val tertiaryContainer = Color(0xFF543F5E)
-    override val onTertiaryContainer = Color(0xFFF6D9FF)
-    override val error = Color(0xFFFFB4AB)
-    override val onError = Color(0xFF690005)
-    override val errorContainer = Color(0xFF93000A)
-    override val onErrorContainer = Color(0xFFFFDAD6)
-    override val background = Color(0xFF1A1C1E)
-    override val onBackground = Color(0xFFE3E2E6)
-    override val surface = Color(0xFF1A1C1E)
-    override val onSurface = Color(0xFFE3E2E6)
-    override val surfaceVariant = Color(0xFF282E35)
-    override val onSurfaceVariant = Color(0xFFC3C6CF)
-    override val surfaceContainerLowest = Color(0xFF212429)
-    override val surfaceContainerLow = Color(0xFF212429)
-    override val surfaceContainer = Color(0xFF252A30)
-    override val surfaceContainerHigh = Color(0xFF282E35)
-    override val surfaceContainerHighest = Color(0xFF2C333C)
-    override val outline = Color(0xFF8D9199)
-    override val outlineVariant = Color(0xFF43474E)
-    override val scrim = Color(0xFF000000)
-    override val inverseSurface = Color(0xFFE3E2E6)
-    override val inverseOnSurface = Color(0xFF1A1C1E)
-    override val inversePrimary = Color(0xFF005FAD)
-}

+ 0 - 60
app/src/blue/java/ch/threema/app/compose/theme/color/ColorsLight.kt

@@ -1,60 +0,0 @@
-/*  _____ _
- * |_   _| |_  _ _ ___ ___ _ __  __ _
- *   | | | ' \| '_/ -_) -_) '  \/ _` |_
- *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
- *
- * Threema for Android
- * Copyright (c) 2024-2025 Threema GmbH
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program 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 Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package ch.threema.app.compose.theme.color
-
-import androidx.compose.ui.graphics.Color
-
-object ColorsLight : ComposeColorPaletteLight() {
-    override val primary = Color(0xFF005FAD)
-    override val onPrimary = Color(0xFFFFFFFF)
-    override val primaryContainer = Color(0xFFD4E3FF)
-    override val onPrimaryContainer = Color(0xFF001C3A)
-    override val secondary = Color(0xFF545F71)
-    override val onSecondary = Color(0xFFFFFFFF)
-    override val secondaryContainer = Color(0xFFE9F5FE)
-    override val onSecondaryContainer = Color(0xFF111C2B)
-    override val tertiary = Color(0xFF6D5676)
-    override val onTertiary = Color(0xFFFFFFFF)
-    override val tertiaryContainer = Color(0xFFF6D9FF)
-    override val onTertiaryContainer = Color(0xFF271430)
-    override val error = Color(0xFFBA1A1A)
-    override val onError = Color(0xFFFFFFFF)
-    override val errorContainer = Color(0xFFFFDAD6)
-    override val onErrorContainer = Color(0xFF410002)
-    override val background = Color(0xFFFDFCFF)
-    override val onBackground = Color(0xFF1A1C1E)
-    override val surface = Color(0xFFFDFCFF)
-    override val onSurface = Color(0xFF1A1C1E)
-    override val surfaceVariant = Color(0xFFE2EBF6)
-    override val onSurfaceVariant = Color(0xFF43474E)
-    override val surfaceContainerLowest = Color(0xFFF0F3FA)
-    override val surfaceContainerLow = Color(0xFFF0F3FA)
-    override val surfaceContainer = Color(0xFFE8EFF8)
-    override val surfaceContainerHigh = Color(0xFFE2EBF6)
-    override val surfaceContainerHighest = Color(0xFFDAE6F3)
-    override val outline = Color(0xFF73777F)
-    override val outlineVariant = Color(0xFFC3C6CF)
-    override val scrim = Color(0xFF000000)
-    override val inverseSurface = Color(0xFF2F3033)
-    override val inverseOnSurface = Color(0xFFF1F0F4)
-    override val inversePrimary = Color(0xFFA4C8FF)
-}

BIN=BIN
app/src/blue/res/drawable-hdpi/ic_notification_multi.png


BIN=BIN
app/src/blue/res/drawable-hdpi/ic_notification_small.png


BIN=BIN
app/src/blue/res/drawable-hdpi/logo_main_white.png


BIN=BIN
app/src/blue/res/drawable-mdpi/ic_notification_multi.png


BIN=BIN
app/src/blue/res/drawable-mdpi/ic_notification_small.png


BIN=BIN
app/src/blue/res/drawable-mdpi/logo_main_white.png


+ 0 - 116
app/src/blue/res/drawable-v24/ic_launcher_foreground.xml

@@ -1,116 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:aapt="http://schemas.android.com/aapt"
-    android:width="108dp"
-    android:height="108dp"
-    android:viewportWidth="1500"
-    android:viewportHeight="1500">
-    <group
-        android:translateX="238"
-        android:translateY="238">
-        <path
-            android:fillColor="#FAFAFA"
-            android:fillType="evenOdd"
-            android:pathData="M0,0h1024v1024h-1024z"
-            android:strokeWidth="1"
-            android:strokeColor="#00000000" />
-        <path
-            android:fillColor="#035EA5"
-            android:fillType="nonZero"
-            android:pathData="M567.8,838.9C567.8,869.9 542.8,894.9 511.8,894.9C480.9,894.9 455.9,869.9 455.9,838.9C455.9,808 480.9,782.9 511.8,782.9C542.8,782.9 567.8,808 567.8,838.9ZM365.9,838.9C365.9,869.9 340.9,894.9 309.9,894.9C278.9,894.9 253.9,869.9 253.9,838.9C253.9,808 279,782.9 309.9,782.9C340.8,782.9 365.9,808 365.9,838.9ZM769.8,838.9C769.8,869.9 744.8,894.9 713.8,894.9C682.8,894.9 657.8,869.9 657.8,838.9C657.8,808 682.9,782.9 713.8,782.9C744.7,782.9 769.8,808 769.8,838.9Z"
-            android:strokeWidth="1"
-            android:strokeColor="#00000000" />
-        <path
-            android:fillType="nonZero"
-            android:pathData="M512,301.4C481,301.4 456,326.4 456,357.3L456,394.6L568,394.6L568,357.3C568,326.4 542.9,301.4 512,301.4L512,301.4Z"
-            android:strokeWidth="1"
-            android:strokeColor="#00000000">
-            <aapt:attr name="android:fillColor">
-                <gradient
-                    android:endX="582.1"
-                    android:endY="392.4"
-                    android:startX="466.1"
-                    android:startY="315.7"
-                    android:type="linear">
-                    <item
-                        android:color="#FF88C6F9"
-                        android:offset="0" />
-                    <item
-                        android:color="#FF5FB1F6"
-                        android:offset="1" />
-                </gradient>
-            </aapt:attr>
-        </path>
-        <path
-            android:fillType="nonZero"
-            android:pathData="M830.9,347.1C790.3,337 747.7,321 702.2,301.4C589.2,252.6 515.1,222.7 444.9,236.1C414,241.9 392.6,251.6 383.5,256.9C374.4,262.2 355.4,285.2 355.4,308.3C355.4,341.4 367.4,371 405.8,397.7C408.4,395.7 411.5,394.6 415,394.6L418.7,394.6L418.7,357.3C418.7,305.9 460.4,264.1 512,264.1C563.5,264.1 605.3,305.9 605.3,357.3L605.3,394.6L609,394.6C617.3,394.6 624,401.3 624,409.6L624,498.9C677.2,531.3 721.2,574.7 728.3,636.7C797.4,583.6 841,505.6 841,418.6C841,393.9 837.5,369.9 830.9,347.1L830.9,347.1Z"
-            android:strokeWidth="1"
-            android:strokeColor="#00000000">
-            <aapt:attr name="android:fillColor">
-                <gradient
-                    android:endX="734.7"
-                    android:endY="587.3"
-                    android:startX="386.9"
-                    android:startY="305.6"
-                    android:type="linear">
-                    <item
-                        android:color="#FF92CBFA"
-                        android:offset="0" />
-                    <item
-                        android:color="#FF389EF3"
-                        android:offset="1" />
-                </gradient>
-            </aapt:attr>
-        </path>
-        <path
-            android:fillType="nonZero"
-            android:pathData="M624,541.1C624,549.4 617.3,556.1 609,556.1L415,556.1C406.7,556.1 400,549.4 400,541.1L400,409.6C400,404.7 402.3,400.5 405.8,397.7C369,369 356.5,341.4 356.5,308.3C356.5,285.2 373.8,263.6 383.5,256.9C323,292.2 271,344.3 186.4,376.9C184.2,390.5 183,404.4 183,418.6C183,478 203.3,533.2 238.2,579.2L203,720.1L367.8,678.9C411.4,697.6 460.3,708.1 512,708.1C594.8,708.1 670.6,681.1 728.4,636.6C722,573.7 678.7,529.5 624,498.9L624,541.1Z"
-            android:strokeWidth="1"
-            android:strokeColor="#00000000">
-            <aapt:attr name="android:fillColor">
-                <gradient
-                    android:endX="498.8"
-                    android:endY="639.9"
-                    android:startX="183"
-                    android:startY="409.8"
-                    android:type="linear">
-                    <item
-                        android:color="#FF3DA7FF"
-                        android:offset="0" />
-                    <item
-                        android:color="#FF1968A8"
-                        android:offset="0.5" />
-                    <item
-                        android:color="#FF105891"
-                        android:offset="0.7" />
-                    <item
-                        android:color="#FF003C6B"
-                        android:offset="1" />
-                </gradient>
-            </aapt:attr>
-        </path>
-        <path
-            android:fillType="nonZero"
-            android:pathData="M512,129.1C346.4,129.1 209.3,236.8 186.4,376.9C271,344.3 323,293.2 383.5,257.9C392.6,252.6 414,242.9 444.9,237C515.1,223.7 589.2,255.6 702.2,304.4C747.7,323.9 790.3,337 830.9,347C794.7,221.8 665.6,129.1 512,129.1Z"
-            android:strokeWidth="1"
-            android:strokeColor="#00000000">
-            <aapt:attr name="android:fillColor">
-                <gradient
-                    android:endX="281"
-                    android:endY="215.7"
-                    android:startX="627.6"
-                    android:startY="296.8"
-                    android:type="linear">
-                    <item
-                        android:color="#FFE0F2FF"
-                        android:offset="0" />
-                    <item
-                        android:color="#FFB9DFFD"
-                        android:offset="1" />
-                    <item
-                        android:color="#FFB9DFFD"
-                        android:offset="1" />
-                </gradient>
-            </aapt:attr>
-        </path>
-    </group>
-</vector>

+ 0 - 19
app/src/blue/res/drawable-v24/ic_launcher_monochrome.xml

@@ -1,19 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="108dp"
-    android:height="108dp"
-    android:viewportWidth="108"
-    android:viewportHeight="108">
-    <group
-        android:scaleX="0.74"
-        android:scaleY="0.74"
-        android:translateX="14.04"
-        android:translateY="14.04">
-        <path
-            android:fillColor="#333333"
-            android:pathData="m58.72,80.98c1.56,-0.53 2.64,-1.78 2.64,-3.7 0,-2.88 -2.23,-4.39 -5.14,-4.39h-8.11v17.04h8.57c3.19,0 5.28,-1.8 5.28,-4.8 0,-2.18 -1.22,-3.53 -3.24,-4.15zM51.47,75.48h3.96c1.75,0 2.66,0.7 2.66,2.06 0,1.37 -0.94,2.21 -2.69,2.21h-3.94zM55.48,87.34h-4.01v-4.97h4.01c2.06,0 3.12,0.98 3.12,2.5 0,1.54 -1.03,2.47 -3.12,2.47z" />
-        <path
-            android:fillColor="#333"
-            android:fillType="evenOdd"
-            android:pathData="M40.84,67.66l-14.89,3.72 3.18,-12.73c-3.15,-4.15 -4.99,-9.14 -4.99,-14.51 0,-14.45 13.31,-26.16 29.73,-26.16s29.73,11.71 29.73,26.16 -13.31,26.16 -29.73,26.16c-4.67,0 -9.09,-0.95 -13.03,-2.64h0ZM45.43,41.97h-0.33c-0.75,0 -1.36,0.61 -1.36,1.36v11.88c0,0.75 0.61,1.36 1.36,1.36h17.53c0.75,0 1.36,-0.61 1.36,-1.36v-11.88c0,-0.75 -0.61,-1.36 -1.36,-1.36h-0.33v-3.37c0,-4.65 -3.77,-8.42 -8.44,-8.42s-8.43,3.77 -8.43,8.42v3.37ZM58.92,41.97h-10.12v-3.37c0,-2.79 2.26,-5.05 5.06,-5.05s5.06,2.27 5.06,5.05v3.37Z" />
-    </group>
-</vector>

+ 29 - 0
app/src/blue/res/drawable-v26/ic_launcher_monochrome.xml

@@ -0,0 +1,29 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="512"
+    android:viewportHeight="512">
+    <group android:scaleX="0.6111111"
+        android:scaleY="0.6111111"
+        android:translateX="99.55556"
+        android:translateY="99.55556">
+        <path
+            android:pathData="M0,0h512v512h-512z"/>
+        <group>
+            <clip-path
+                android:pathData="M102.81,59.06h304.76v391.61h-304.76z"/>
+            <path
+                android:pathData="M174.93,450.66C189.08,450.66 200.54,439.2 200.54,425.05C200.54,410.91 189.08,399.44 174.93,399.44C160.79,399.44 149.32,410.91 149.32,425.05C149.32,439.2 160.79,450.66 174.93,450.66Z"
+                android:fillColor="#000000"/>
+            <path
+                android:pathData="M258.53,450.66C272.67,450.66 284.14,439.2 284.14,425.05C284.14,410.91 272.67,399.44 258.53,399.44C244.39,399.44 232.92,410.91 232.92,425.05C232.92,439.2 244.39,450.66 258.53,450.66Z"
+                android:fillColor="#000000"/>
+            <path
+                android:pathData="M342.21,450.66C356.36,450.66 367.82,439.2 367.82,425.05C367.82,410.91 356.36,399.44 342.21,399.44C328.07,399.44 316.6,410.91 316.6,425.05C316.6,439.2 328.07,450.66 342.21,450.66Z"
+                android:fillColor="#000000"/>
+            <path
+                android:pathData="M255.38,59C339.4,59 407.84,127.35 407.84,211.38C407.84,295.4 339.49,363.76 255.46,363.76C224.46,363.76 195.68,354.5 171.61,338.57L113.02,357.33L132.04,300.71C113.79,275.62 103,244.7 103,211.38C103,127.35 171.35,59 255.38,59ZM205.3,287H263.65C275.61,287 285.66,285.36 293.8,282.08C302,278.8 308.19,273.91 312.36,267.42C316.52,260.85 318.61,252.72 318.61,243.01C318.61,237.13 317.35,231.56 314.82,226.3C312.29,221.03 308.15,216.79 302.41,213.58C300.07,212.23 297.41,211.17 294.42,210.41C295.84,209.86 297.17,209.24 298.41,208.56C304.36,205.28 308.66,201 311.33,195.74C314.06,190.48 315.43,184.8 315.43,178.72C315.43,171.75 314.13,165.7 311.54,160.57C308.94,155.44 305.14,151.17 300.15,147.75C295.23,144.33 289.25,141.8 282.21,140.16C275.17,138.52 267.17,137.7 258.21,137.7H205.3V287ZM264.98,222.5C269.9,222.5 273.83,223.32 276.77,224.96C279.78,226.6 281.97,228.93 283.34,231.94C284.77,234.94 285.49,238.5 285.49,242.6C285.49,246.09 284.74,249.23 283.23,252.03C281.73,254.84 279.37,257.06 276.16,258.7C272.95,260.34 268.78,261.16 263.65,261.16H238.73V222.5H264.98ZM258.21,163.65C263.75,163.65 268.26,164.29 271.75,165.59C275.3,166.82 277.9,168.81 279.54,171.54C281.18,174.27 282,177.86 282,182.31C282,186.2 281.18,189.49 279.54,192.15C277.9,194.75 275.3,196.73 271.75,198.1C268.26,199.47 263.72,200.15 258.11,200.15H238.73V163.65H258.21Z"
+                android:fillColor="#000000"/>
+        </group>
+    </group>
+</vector>

BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_01_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_02_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_03_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_04_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_05_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_06_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_07_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_08_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_09_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_10_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_11_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_12_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_13_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_14_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_15_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_16_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_17_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_18_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_19_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_20_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_21_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_22_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_23_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_24_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_25_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_26_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_27_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_28_40ms.png


BIN=BIN
app/src/blue/res/drawable-xhdpi/anim_29_40ms.png


Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio