Threema 10 цаг өмнө
parent
commit
1e8ce857f3
87 өөрчлөгдсөн 732 нэмэгдсэн , 537 устгасан
  1. 1 1
      app/assets/license.html
  2. 6 2
      app/build.gradle.kts
  3. 1 1
      app/src/foss_based/assets/license.html
  4. 1 1
      app/src/libre/play/release-notes/de/default.txt
  5. 1 1
      app/src/libre/play/release-notes/en-US/default.txt
  6. 1 0
      app/src/main/AndroidManifest.xml
  7. 1 1
      app/src/main/java/ch/threema/app/activities/AddContactActivity.java
  8. 2 8
      app/src/main/java/ch/threema/app/activities/EditSendContactActivity.kt
  9. 2 0
      app/src/main/java/ch/threema/app/activities/ThreemaToolbarActivity.java
  10. 4 2
      app/src/main/java/ch/threema/app/activities/wizard/WizardBaseActivity.java
  11. 6 1
      app/src/main/java/ch/threema/app/adapters/ComposeMessageAdapter.java
  12. 0 2
      app/src/main/java/ch/threema/app/di/DependencyContainer.kt
  13. 2 1
      app/src/main/java/ch/threema/app/emojis/EmojiPicker.java
  14. 1 1
      app/src/main/java/ch/threema/app/errorreporting/ErrorReportingModule.kt
  15. 6 2
      app/src/main/java/ch/threema/app/errorreporting/SentryService.kt
  16. 9 2
      app/src/main/java/ch/threema/app/errorreporting/ThreemaUncaughtExceptionHandler.kt
  17. 4 1
      app/src/main/java/ch/threema/app/fragments/DistributionListFragment.kt
  18. 4 1
      app/src/main/java/ch/threema/app/fragments/GroupListFragment.kt
  19. 0 122
      app/src/main/java/ch/threema/app/fragments/RecentListFragment.java
  20. 86 0
      app/src/main/java/ch/threema/app/fragments/RecentListFragment.kt
  21. 10 3
      app/src/main/java/ch/threema/app/fragments/RecipientListFragment.java
  22. 9 6
      app/src/main/java/ch/threema/app/fragments/UserListFragment.kt
  23. 6 9
      app/src/main/java/ch/threema/app/fragments/WorkUserListFragment.kt
  24. 1 1
      app/src/main/java/ch/threema/app/home/HomeActivity.kt
  25. 48 43
      app/src/main/java/ch/threema/app/mediaattacher/EditSendContactViewModel.kt
  26. 13 1
      app/src/main/java/ch/threema/app/mediaattacher/MediaAttacherFeatureModule.kt
  27. 2 3
      app/src/main/java/ch/threema/app/notifications/ConversationNotificationConverter.kt
  28. 0 2
      app/src/main/java/ch/threema/app/preference/SettingsAppearanceFragment.kt
  29. 0 1
      app/src/main/java/ch/threema/app/preference/SettingsCallsFragment.kt
  30. 0 5
      app/src/main/java/ch/threema/app/preference/SettingsMediaFragment.kt
  31. 0 2
      app/src/main/java/ch/threema/app/preference/SettingsPrivacyFragment.kt
  32. 15 10
      app/src/main/java/ch/threema/app/preference/SettingsSummaryFragment.kt
  33. 10 0
      app/src/main/java/ch/threema/app/services/AudioPlayerService.kt
  34. 6 1
      app/src/main/java/ch/threema/app/services/notification/NotificationServiceImpl.kt
  35. 14 5
      app/src/main/java/ch/threema/app/shortcuts/ShortcutsUpdaterMonitor.kt
  36. 4 0
      app/src/main/java/ch/threema/app/startup/AppStartupMonitorImpl.kt
  37. 2 4
      app/src/main/java/ch/threema/app/startup/components/ErrorState.kt
  38. 3 3
      app/src/main/java/ch/threema/app/tasks/OnFSFeatureMaskDowngradedTask.kt
  39. 1 1
      app/src/main/java/ch/threema/app/threemasafe/BackupThreemaSafeFragment.java
  40. 1 1
      app/src/main/java/ch/threema/app/threemasafe/ThreemaSafeConfigureActivity.java
  41. 1 5
      app/src/main/java/ch/threema/app/threemasafe/ThreemaSafeService.java
  42. 7 32
      app/src/main/java/ch/threema/app/threemasafe/ThreemaSafeServiceImpl.java
  43. 11 2
      app/src/main/java/ch/threema/app/utils/ConversationNotificationUtil.java
  44. 22 11
      app/src/main/java/ch/threema/app/voicemessage/VoiceRecorderViewModel.kt
  45. 13 6
      app/src/main/java/ch/threema/app/voip/groupcall/service/GroupCallControllerImpl.kt
  46. 2 0
      app/src/main/java/ch/threema/app/voip/groupcall/sfu/GroupCallController.kt
  47. 18 9
      app/src/main/java/ch/threema/app/voip/viewmodel/GroupCallViewModel.kt
  48. 13 4
      app/src/main/java/ch/threema/app/webviews/PrivacyPolicyActivity.kt
  49. 0 141
      app/src/main/java/ch/threema/app/webviews/SimpleWebViewActivity.java
  50. 119 0
      app/src/main/java/ch/threema/app/webviews/SimpleWebViewActivity.kt
  51. 9 4
      app/src/main/java/ch/threema/app/workers/ThreemaSafeUploadWorker.kt
  52. BIN
      app/src/main/res/drawable-hdpi/ic_chat_bubble_bitmap_24dp.png
  53. BIN
      app/src/main/res/drawable-mdpi/ic_chat_bubble_bitmap_24dp.png
  54. BIN
      app/src/main/res/drawable-xhdpi/ic_chat_bubble_bitmap_24dp.png
  55. BIN
      app/src/main/res/drawable-xxhdpi/ic_chat_bubble_bitmap_24dp.png
  56. 34 24
      app/src/main/res/layout/item_widget.xml
  57. 26 25
      app/src/main/res/layout/widget_messages.xml
  58. 3 1
      app/src/main/res/values-be-rBY/strings.xml
  59. 94 4
      app/src/main/res/values-bg/strings.xml
  60. 6 0
      app/src/main/res/values-bg/voip_strings.xml
  61. 1 0
      app/src/main/res/values-ca/strings.xml
  62. 2 0
      app/src/main/res/values-cs/strings.xml
  63. 2 0
      app/src/main/res/values-de/strings.xml
  64. 2 0
      app/src/main/res/values-es/strings.xml
  65. 2 0
      app/src/main/res/values-fr/strings.xml
  66. 4 2
      app/src/main/res/values-gsw/strings.xml
  67. 1 0
      app/src/main/res/values-hu/strings.xml
  68. 2 0
      app/src/main/res/values-it/strings.xml
  69. 1 0
      app/src/main/res/values-ja/strings.xml
  70. 10 0
      app/src/main/res/values-night/colors.xml
  71. 1 0
      app/src/main/res/values-nl-rNL/strings.xml
  72. 1 0
      app/src/main/res/values-no/strings.xml
  73. 1 0
      app/src/main/res/values-pl/strings.xml
  74. 1 0
      app/src/main/res/values-pt-rBR/strings.xml
  75. 2 0
      app/src/main/res/values-ru/strings.xml
  76. 1 0
      app/src/main/res/values-sk/strings.xml
  77. 6 2
      app/src/main/res/values-tr/strings.xml
  78. 5 3
      app/src/main/res/values-uk/strings.xml
  79. 2 0
      app/src/main/res/values-zh-rCN/strings.xml
  80. 2 0
      app/src/main/res/values-zh-rTW/strings.xml
  81. 9 0
      app/src/main/res/values/colors.xml
  82. 5 3
      app/src/main/res/values/strings.xml
  83. 2 2
      app/src/test/java/ch/threema/app/errorreporting/ErrorReportDetailsProviderTest.kt
  84. 1 1
      app/src/test/java/ch/threema/app/errorreporting/SentryServiceTest.kt
  85. 2 2
      app/src/test/resources/api/requests/sentry-envelope-with-exception.txt
  86. 2 2
      app/src/test/resources/api/requests/sentry-envelope-with-message.txt
  87. 1 1
      gradle/libs.versions.toml

+ 1 - 1
app/assets/license.html

@@ -58,7 +58,7 @@
 <h1>Translations</h1>
 
 <p>
-    The app localizations were realized with kind support from various translators.<br/>
+    The app localizations were realized with kind support from various translators. Special thanks to Daniel Axelsson, Max Bakalov and Daryna Stremetska, Honhatduy, Ondřej Nový, and Toni Petkov.<br/>
     Thank you!
 </p>
 

+ 6 - 2
app/build.gradle.kts

@@ -33,7 +33,7 @@ if (gradle.startParameter.taskRequests.toString().contains("Hms")) {
 /**
  * Only use the scheme "<major>.<minor>.<patch>" for the appVersion
  */
-val appVersion = "6.5.1"
+val appVersion = "6.5.2"
 
 /**
  * betaSuffix with leading dash (e.g. `-beta1`).
@@ -42,7 +42,7 @@ val appVersion = "6.5.1"
  */
 val betaSuffix = ""
 
-val defaultVersionCode = 1210
+val defaultVersionCode = 1212
 
 /**
  * Map with keystore paths (if found).
@@ -79,7 +79,11 @@ android {
         setProductNames(
             appName = "Threema",
         )
+
+        stringBuildConfigField("BASE_VERSION_NAME", appVersion)
         intBuildConfigField("DEFAULT_VERSION_CODE", defaultVersionCode)
+        stringBuildConfigField("VERSION_NAME_BETA_SUFFIX", betaSuffix)
+
         // package name used for sync adapter - needs to match mime types below
         stringResValue("package_name", applicationId!!)
         stringResValue("contacts_mime_type", "vnd.android.cursor.item/vnd.$applicationId.profile")

+ 1 - 1
app/src/foss_based/assets/license.html

@@ -58,7 +58,7 @@
 <h1>Translations</h1>
 
 <p>
-    The app localizations were realized with kind support from various translators.<br/>
+    The app localizations were realized with kind support from various translators. Special thanks to Daniel Axelsson, Max Bakalov and Daryna Stremetska, Honhatduy, Ondřej Nový, and Toni Petkov.<br/>
     Thank you!
 </p>
 

+ 1 - 1
app/src/libre/play/release-notes/de/default.txt

@@ -1,2 +1,2 @@
-- Unterstützung für Emojis der Version 17.0
+- Behebung eines Fehlers beim Drehen des Smartphones während der Nutzung der Kamera in Threema
 - Verbesserungen und Behebung verschiedener Fehler

+ 1 - 1
app/src/libre/play/release-notes/en-US/default.txt

@@ -1,2 +1,2 @@
-- Support for emoji version 17.0
+- Fixed a bug that occurred when rotating the smartphone while using Threema’s camera
 - Various improvements and bug fixes

+ 1 - 0
app/src/main/AndroidManifest.xml

@@ -780,6 +780,7 @@
             android:name=".camera.CameraActivity"
             android:autoRemoveFromRecents="true"
             android:clearTaskOnLaunch="true"
+            android:screenOrientation="fullSensor"
             android:theme="@style/Theme.Threema.Camera">
             <!-- Declare Huawei notch support -->
             <meta-data

+ 1 - 1
app/src/main/java/ch/threema/app/activities/AddContactActivity.java

@@ -82,7 +82,7 @@ public class AddContactActivity extends ThreemaActivity implements GenericAlertD
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         logScreenVisibility(this, logger);
-        if (dependencies.getIdentityStore().getIdentityString() == null) {
+        if (!dependencies.getIdentityProvider().hasIdentity()) {
             finish();
             return;
         }

+ 2 - 8
app/src/main/java/ch/threema/app/activities/EditSendContactActivity.kt

@@ -11,7 +11,6 @@ import android.net.Uri
 import android.os.Bundle
 import android.os.Handler
 import android.os.Looper
-import android.text.format.DateFormat
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewTreeObserver
@@ -29,7 +28,6 @@ import ch.threema.app.R
 import ch.threema.app.mediaattacher.EditSendContactViewModel
 import ch.threema.app.ui.VCardPropertyView
 import ch.threema.app.ui.setMargin
-import ch.threema.app.utils.VCardExtractor
 import ch.threema.app.utils.logScreenVisibility
 import ch.threema.base.utils.getThreemaLogger
 import com.google.android.material.appbar.AppBarLayout
@@ -167,11 +165,7 @@ class EditSendContactActivity : ThreemaToolbarActivity() {
             return
         }
 
-        viewModel.initializeContact(
-            contactUri,
-            contentResolver,
-            VCardExtractor(DateFormat.getDateFormat(applicationContext), resources),
-        )
+        viewModel.initializeContact(contactUri)
 
         // Show edit-texts for the name properties that are set in the contact
         val editTexts = listOf(
@@ -212,7 +206,7 @@ class EditSendContactActivity : ThreemaToolbarActivity() {
             // Send the possibly modified VCard as file
             findViewById<FloatingActionButton>(R.id.send_contact).apply {
                 setOnClickListener {
-                    viewModel.prepareFinalVCard(context, cacheDir, contactUri)
+                    viewModel.prepareFinalVCard(contactUri)
                 }
                 visibility = View.VISIBLE
             }

+ 2 - 0
app/src/main/java/ch/threema/app/activities/ThreemaToolbarActivity.java

@@ -46,6 +46,7 @@ public abstract class ThreemaToolbarActivity extends ThreemaActivity {
 
     private AppBarLayout appBarLayout;
     private Toolbar toolbar;
+    @Nullable
     private View connectionIndicator;
 
     @NonNull
@@ -159,6 +160,7 @@ public abstract class ThreemaToolbarActivity extends ThreemaActivity {
         return this.toolbar;
     }
 
+    @Nullable
     protected View getConnectionIndicator() {
         return connectionIndicator;
     }

+ 4 - 2
app/src/main/java/ch/threema/app/activities/wizard/WizardBaseActivity.java

@@ -383,7 +383,9 @@ public class WizardBaseActivity extends ThreemaAppCompatActivity implements
 
     @Override
     protected void onDestroy() {
-        viewPager.removeOnPageChangeListener(this);
+        if (viewPager != null) {
+            viewPager.removeOnPageChangeListener(this);
+        }
 
         super.onDestroy();
     }
@@ -1060,7 +1062,7 @@ public class WizardBaseActivity extends ThreemaAppCompatActivity implements
                         dependencies.getThreemaSafeService().storeMasterKey(masterKey);
                         dependencies.getPreferenceService().setThreemaSafeServerInfo(safeServerInfo);
                         dependencies.getThreemaSafeService().setEnabled(true);
-                        dependencies.getThreemaSafeService().uploadNow(true);
+                        dependencies.getThreemaSafeService().uploadNow();
                     } else {
                         Toast.makeText(WizardBaseActivity.this, R.string.safe_error_preparing, Toast.LENGTH_LONG).show();
                     }

+ 6 - 1
app/src/main/java/ch/threema/app/adapters/ComposeMessageAdapter.java

@@ -1180,7 +1180,12 @@ public class ComposeMessageAdapter extends ArrayAdapter<AbstractMessageModel> im
                 int n = 0;
                 final int targetSize = targetMessageModels.size();
                 final int firstVisiblePosition = listView.getFirstVisiblePosition();
-                for (int i = firstVisiblePosition, j = listView.getLastVisiblePosition(); i <= j; i++) {
+                // The visible positions are based on the last layout pass and may point beyond the
+                // current item count if the list has shrunk since (e.g. after emptying the chat).
+                // Accessing such a position would throw an IndexOutOfBoundsException, so clamp the
+                // range to the current count.
+                final int lastValidPosition = Math.min(listView.getLastVisiblePosition(), listView.getCount() - 1);
+                for (int i = firstVisiblePosition; i <= lastValidPosition; i++) {
                     AbstractMessageModel messageModel = (AbstractMessageModel) listView.getItemAtPosition(i);
                     if (messageModel != null) {
                         for (AbstractMessageModel targetMessageModel : targetMessageModels) {

+ 0 - 2
app/src/main/java/ch/threema/app/di/DependencyContainer.kt

@@ -47,7 +47,6 @@ import ch.threema.data.repositories.GroupModelRepository
 import ch.threema.domain.protocol.api.APIConnector
 import ch.threema.domain.protocol.connection.ServerConnection
 import ch.threema.domain.stores.DHSessionStore
-import ch.threema.domain.stores.IdentityStore
 import ch.threema.domain.taskmanager.TaskManager
 import ch.threema.storage.factories.ContactModelFactory
 import ch.threema.storage.factories.WebClientSessionModelFactory
@@ -83,7 +82,6 @@ class DependencyContainer : KoinComponent {
     val groupModelRepository: GroupModelRepository by inject()
     val groupService: GroupService by inject()
     val identityProvider: IdentityProvider by inject()
-    val identityStore: IdentityStore by inject()
     val licenseService: LicenseService<*> by inject()
     val lifetimeService: LifetimeService by inject()
     val localeService: LocaleService by inject()

+ 2 - 1
app/src/main/java/ch/threema/app/emojis/EmojiPicker.java

@@ -432,7 +432,8 @@ public class EmojiPicker extends LinearLayout implements EmojiSearchWidget.Emoji
     }
 
     public boolean isEmojiSearchShown() {
-        return emojiSearchWidget.isShown();
+        var emojiSearchWidget = this.emojiSearchWidget;
+        return emojiSearchWidget != null && emojiSearchWidget.isShown();
     }
 
     public interface EmojiPickerListener {

+ 1 - 1
app/src/main/java/ch/threema/app/errorreporting/ErrorReportingModule.kt

@@ -32,7 +32,7 @@ val errorReportingModule = module {
     factory {
         SentryService.MetaInfo(
             androidSdkVersion = Build.VERSION.SDK_INT,
-            appVersion = BuildConfig.VERSION_NAME,
+            release = "threema-android@${BuildConfig.BASE_VERSION_NAME}${BuildConfig.VERSION_NAME_BETA_SUFFIX}",
             versionCode = BuildConfig.DEFAULT_VERSION_CODE,
             buildFlavor = BuildFlavor.current.fullDisplayName,
             deviceModel = Build.MODEL,

+ 6 - 2
app/src/main/java/ch/threema/app/errorreporting/SentryService.kt

@@ -77,7 +77,7 @@ class SentryService(
         val event = Event(
             eventId = record.id.format(),
             timestamp = record.createdAt.toString(),
-            release = metaInfo.appVersion,
+            release = metaInfo.release,
             dist = metaInfo.versionCode.toString(),
             tags = Tags(
                 androidVersion = metaInfo.androidSdkVersion.toString(),
@@ -239,10 +239,14 @@ class SentryService(
         val publicApiKey: String,
     )
 
+    /**
+     * @param release Must be unique across all Sentry projects, as specified by Sentry's documentation.
+     * See https://develop.sentry.dev/sdk/foundations/envelopes/event-payloads/
+     */
     data class MetaInfo(
         val deviceModel: String,
         val androidSdkVersion: Int,
-        val appVersion: String,
+        val release: String,
         val versionCode: Int,
         val buildFlavor: String,
     )

+ 9 - 2
app/src/main/java/ch/threema/app/errorreporting/ThreemaUncaughtExceptionHandler.kt

@@ -1,6 +1,7 @@
 package ch.threema.app.errorreporting
 
 import android.content.Context
+import android.os.DeadObjectException
 import ch.threema.app.BuildConfig
 import ch.threema.common.TimeProvider
 import ch.threema.common.UUIDGenerator
@@ -14,14 +15,20 @@ class ThreemaUncaughtExceptionHandler(
     override fun uncaughtException(t: Thread, e: Throwable) {
         UncaughtExceptionsLogger.logUnhandledException(e)
 
-        @Suppress("KotlinConstantConditions")
-        if (BuildConfig.ERROR_REPORTING_SUPPORTED) {
+        if (shouldCreateErrorReport(e)) {
             storeExceptionForErrorReporting(e)
         }
 
         defaultHandler?.uncaughtException(t, e)
     }
 
+    private fun shouldCreateErrorReport(e: Throwable): Boolean {
+        if (!BuildConfig.ERROR_REPORTING_SUPPORTED) {
+            return false
+        }
+        return e !is DeadObjectException && e.cause !is DeadObjectException
+    }
+
     private fun storeExceptionForErrorReporting(e: Throwable) {
         // Intentionally not using Koin here, as it might not be initialized yet at this point
         ErrorRecordStoreImpl.create(

+ 4 - 1
app/src/main/java/ch/threema/app/fragments/DistributionListFragment.kt

@@ -4,6 +4,7 @@ import androidx.lifecycle.lifecycleScope
 import ch.threema.app.R
 import ch.threema.app.activities.DistributionListAddActivity
 import ch.threema.app.adapters.DistributionListAdapter
+import ch.threema.app.di.awaitAppFullyReady
 import ch.threema.app.preference.service.PreferenceService
 import ch.threema.app.services.DistributionListService
 import ch.threema.common.DispatcherProvider
@@ -28,8 +29,10 @@ class DistributionListFragment : RecipientListFragment() {
 
     override fun getAddIntent() = DistributionListAddActivity.createIntent(requireContext())
 
-    override fun createListAdapter(checkedItemPositions: ArrayList<Int?>?) {
+    override fun createListAdapter(checkedItemPositions: ArrayList<Int>?) {
         lifecycleScope.launch {
+            awaitAppFullyReady()
+
             val distributionListModels = withContext(dispatcherProvider.io) {
                 distributionListService.getAll(
                     object : DistributionListService.DistributionListFilter {

+ 4 - 1
app/src/main/java/ch/threema/app/fragments/GroupListFragment.kt

@@ -4,6 +4,7 @@ import androidx.lifecycle.lifecycleScope
 import ch.threema.app.R
 import ch.threema.app.activities.GroupAddActivity
 import ch.threema.app.adapters.GroupListAdapter
+import ch.threema.app.di.awaitAppFullyReady
 import ch.threema.app.preference.service.PreferenceService
 import ch.threema.app.services.GroupService
 import ch.threema.app.services.GroupService.GroupFilter
@@ -29,8 +30,10 @@ class GroupListFragment : RecipientListFragment() {
 
     override fun getAddIntent() = GroupAddActivity.createIntent(requireContext())
 
-    override fun createListAdapter(checkedItemPositions: ArrayList<Int?>?) {
+    override fun createListAdapter(checkedItemPositions: ArrayList<Int>?) {
         lifecycleScope.launch {
+            awaitAppFullyReady()
+
             val groupModels = withContext(dispatcherProvider.io) {
                 groupService.getAll(
                     object : GroupFilter {

+ 0 - 122
app/src/main/java/ch/threema/app/fragments/RecentListFragment.java

@@ -1,122 +0,0 @@
-package ch.threema.app.fragments;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import androidx.annotation.NonNull;
-import ch.threema.app.R;
-import ch.threema.app.activities.RecipientListBaseActivity;
-import ch.threema.app.adapters.RecentListAdapter;
-import ch.threema.app.preference.service.PreferenceService;
-import ch.threema.app.services.ContactService;
-import ch.threema.app.services.ConversationService;
-import ch.threema.app.services.DistributionListService;
-import ch.threema.app.services.GroupService;
-import ch.threema.storage.models.ConversationModel;
-import kotlin.Lazy;
-
-import static org.koin.java.KoinJavaComponent.inject;
-
-public class RecentListFragment extends RecipientListFragment {
-
-    private final Lazy<PreferenceService> preferenceServiceLazy = inject(PreferenceService.class);
-    private final Lazy<ConversationService> conversationServiceLazy = inject(ConversationService.class);
-    private final Lazy<ContactService> contactServiceLazy = inject(ContactService.class);
-    private final Lazy<GroupService> groupServiceLazy = inject(GroupService.class);
-    private final Lazy<DistributionListService> distributionListServiceLazy = inject(DistributionListService.class);
-
-    private boolean showDistributionLists;
-
-    @Override
-    protected boolean isMultiSelectAllowed() {
-        return multiSelect;
-    }
-
-    @Override
-    protected String getBundleName() {
-        return "RecentListState";
-    }
-
-    @Override
-    protected int getEmptyText() {
-        return R.string.no_recent_conversations;
-    }
-
-    @Override
-    protected int getAddIcon() {
-        return 0;
-    }
-
-    @Override
-    protected int getAddText() {
-        return 0;
-    }
-
-    @Override
-    protected Intent getAddIntent() {
-        return null;
-    }
-
-    @Override
-    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
-        showDistributionLists = ((RecipientListBaseActivity) getActivity()).getShowDistributionLists();
-
-        return super.onCreateView(inflater, container, savedInstanceState);
-    }
-
-    @Override
-    protected void createListAdapter(ArrayList<Integer> checkedItemPositions) {
-        ConversationService.Filter filter = new ConversationService.Filter() {
-            @Override
-            public boolean noDistributionLists() {
-                return !showDistributionLists;
-            }
-
-            @Override
-            public boolean noHiddenConversations() {
-                return preferenceServiceLazy.getValue().arePrivateChatsHidden();
-            }
-
-            @Override
-            public boolean noInvalid() {
-                return true;
-            }
-        };
-
-        //create a copied list!
-        final List<ConversationModel> all;
-        final List<ConversationModel> original = conversationServiceLazy.getValue().getAll(false, filter);
-
-        synchronized (original) {
-            //create a copy)
-            all = new ArrayList<>(original.size());
-            all.addAll(original);
-        }
-
-        adapter = new RecentListAdapter(
-            activity,
-            all,
-            checkedItemPositions,
-            contactServiceLazy.getValue(),
-            groupServiceLazy.getValue(),
-            distributionListServiceLazy.getValue(),
-            preferenceServiceLazy.getValue(),
-            this
-        );
-        setListAdapter(adapter);
-
-        if (listInstanceState != null) {
-            if (isAdded() && getView() != null && getActivity() != null) {
-                getListView().onRestoreInstanceState(listInstanceState);
-            }
-            listInstanceState = null;
-            restoreCheckedItems(checkedItemPositions);
-        }
-    }
-}

+ 86 - 0
app/src/main/java/ch/threema/app/fragments/RecentListFragment.kt

@@ -0,0 +1,86 @@
+package ch.threema.app.fragments
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.lifecycle.lifecycleScope
+import ch.threema.app.R
+import ch.threema.app.activities.RecipientListBaseActivity
+import ch.threema.app.adapters.RecentListAdapter
+import ch.threema.app.di.awaitAppFullyReady
+import ch.threema.app.preference.service.PreferenceService
+import ch.threema.app.services.ContactService
+import ch.threema.app.services.ConversationService
+import ch.threema.app.services.DistributionListService
+import ch.threema.app.services.GroupService
+import ch.threema.common.DispatcherProvider
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.koin.android.ext.android.inject
+
+class RecentListFragment : RecipientListFragment() {
+    private val preferenceService: PreferenceService by inject()
+    private val conversationService: ConversationService by inject()
+    private val contactService: ContactService by inject()
+    private val groupService: GroupService by inject()
+    private val distributionListService: DistributionListService by inject()
+    private val dispatcherProvider: DispatcherProvider by inject()
+
+    private var showDistributionLists = false
+
+    override fun isMultiSelectAllowed() = multiSelect
+
+    override fun getBundleName() = "RecentListState"
+
+    override fun getEmptyText() = R.string.no_recent_conversations
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?,
+    ): View? {
+        showDistributionLists = (getActivity() as RecipientListBaseActivity).showDistributionLists
+        return super.onCreateView(inflater, container, savedInstanceState)
+    }
+
+    override fun createListAdapter(checkedItemPositions: ArrayList<Int>?) {
+        lifecycleScope.launch {
+            awaitAppFullyReady()
+
+            val filter: ConversationService.Filter = object : ConversationService.Filter {
+                override fun noDistributionLists(): Boolean =
+                    !showDistributionLists
+
+                override fun noHiddenConversations(): Boolean =
+                    preferenceService.arePrivateChatsHidden()
+
+                override fun noInvalid() = true
+            }
+
+            val conversations = withContext(dispatcherProvider.io) {
+                conversationService.getAll(false, filter)
+            }
+
+            adapter = RecentListAdapter(
+                activity,
+                conversations,
+                checkedItemPositions,
+                contactService,
+                groupService,
+                distributionListService,
+                preferenceService,
+                this@RecentListFragment,
+            )
+            setListAdapter(adapter)
+
+            if (listInstanceState != null) {
+                if (isAdded && view != null && getActivity() != null) {
+                    listView.onRestoreInstanceState(listInstanceState)
+                }
+                listInstanceState = null
+                restoreCheckedItems(checkedItemPositions)
+            }
+        }
+    }
+}

+ 10 - 3
app/src/main/java/ch/threema/app/fragments/RecipientListFragment.java

@@ -452,11 +452,18 @@ public abstract class RecipientListFragment extends ListFragment implements List
 
     protected abstract @StringRes int getEmptyText();
 
-    protected abstract @DrawableRes int getAddIcon();
+    protected @DrawableRes int getAddIcon() {
+        return 0;
+    }
 
-    protected abstract @StringRes int getAddText();
+    protected @StringRes int getAddText() {
+        return 0;
+    }
 
-    protected abstract Intent getAddIntent();
+    @Nullable
+    protected Intent getAddIntent() {
+        return null;
+    }
 
     protected abstract boolean isMultiSelectAllowed();
 }

+ 9 - 6
app/src/main/java/ch/threema/app/fragments/UserListFragment.kt

@@ -8,6 +8,7 @@ import ch.threema.app.R
 import ch.threema.app.ThreemaApplication
 import ch.threema.app.activities.AddContactActivity
 import ch.threema.app.adapters.UserListAdapter
+import ch.threema.app.di.awaitAppFullyReady
 import ch.threema.app.preference.service.PreferenceService
 import ch.threema.app.services.BlockedIdentitiesService
 import ch.threema.app.services.ContactService
@@ -28,13 +29,13 @@ class UserListFragment : RecipientListFragment() {
 
     override fun isMultiSelectAllowed(): Boolean = multiSelect || multiSelectIdentity
 
-    override fun getBundleName(): String = "UserListState"
+    override fun getBundleName() = "UserListState"
 
-    override fun getEmptyText(): Int = R.string.no_matching_contacts
+    override fun getEmptyText() = R.string.no_matching_contacts
 
-    override fun getAddIcon(): Int = R.drawable.ic_person_add_outline
+    override fun getAddIcon() = R.drawable.ic_person_add_outline
 
-    override fun getAddText(): Int = R.string.menu_add_contact
+    override fun getAddText() = R.string.menu_add_contact
 
     override fun getAddIntent(): Intent = buildActivityIntent<AddContactActivity>(requireActivity()) {
         putExtra(AddContactActivity.EXTRA_ADD_BY_ID, true)
@@ -43,14 +44,16 @@ class UserListFragment : RecipientListFragment() {
     @SuppressLint("StaticFieldLeak")
     override fun createListAdapter(checkedItemPositions: ArrayList<Int>?) {
         lifecycleScope.launch {
-            val contactModels = withContext(dispatcherProvider.worker) {
+            awaitAppFullyReady()
+
+            val contactModels = withContext(dispatcherProvider.io) {
                 if (ConfigUtils.isWorkBuild()) {
                     // Excluding invalid and work contacts
                     contactService
                         .getAllDisplayed(ContactService.ContactSelection.EXCLUDE_INVALID)
                         .filterNot { contactModel -> contactModel.isWorkVerified }
                 } else {
-                    // Excluding invalid contacts because they cannot receive messages anyways
+                    // Excluding invalid contacts because they cannot receive messages anyway
                     contactService.getAllDisplayed(ContactService.ContactSelection.EXCLUDE_INVALID)
                 }
             }

+ 6 - 9
app/src/main/java/ch/threema/app/fragments/WorkUserListFragment.kt

@@ -15,6 +15,7 @@ import ch.threema.app.R
 import ch.threema.app.ThreemaApplication
 import ch.threema.app.activities.directory.DirectoryActivity
 import ch.threema.app.adapters.UserListAdapter
+import ch.threema.app.di.awaitAppFullyReady
 import ch.threema.app.preference.service.PreferenceService
 import ch.threema.app.services.BlockedIdentitiesService
 import ch.threema.app.services.ContactService
@@ -43,17 +44,11 @@ class WorkUserListFragment : RecipientListFragment() {
     private val preferenceService: PreferenceService by inject()
     private val dispatcherProvider: DispatcherProvider by inject()
 
-    override fun isMultiSelectAllowed(): Boolean = multiSelect || multiSelectIdentity
+    override fun isMultiSelectAllowed() = multiSelect || multiSelectIdentity
 
-    override fun getBundleName(): String = "WorkerUserListState"
+    override fun getBundleName() = "WorkerUserListState"
 
-    override fun getEmptyText(): Int = R.string.no_matching_work_contacts
-
-    override fun getAddIcon(): Int = 0
-
-    override fun getAddText(): Int = 0
-
-    override fun getAddIntent(): Intent? = null
+    override fun getEmptyText() = R.string.no_matching_work_contacts
 
     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
         val view = super.onCreateView(inflater, container, savedInstanceState)
@@ -87,6 +82,8 @@ class WorkUserListFragment : RecipientListFragment() {
     @SuppressLint("StaticFieldLeak")
     override fun createListAdapter(checkedItemPositions: ArrayList<Int>?) {
         lifecycleScope.launch {
+            awaitAppFullyReady()
+
             val contactModels = withContext(dispatcherProvider.worker) {
                 contactService.find(
                     object : ContactService.Filter {

+ 1 - 1
app/src/main/java/ch/threema/app/home/HomeActivity.kt

@@ -459,7 +459,7 @@ class HomeActivity : ThreemaAppCompatActivity(), SMSVerificationDialogCallback,
                 threemaSafeService.storeMasterKey(masterKey)
                 preferenceService.setThreemaSafeServerInfo(mdmConfig.getServerInfo())
                 threemaSafeService.setEnabled(true)
-                threemaSafeService.uploadNow(true)
+                threemaSafeService.uploadNow()
             } else {
                 showToast(R.string.safe_error_preparing, ToastDuration.LONG)
             }

+ 48 - 43
app/src/main/java/ch/threema/app/mediaattacher/EditSendContactViewModel.kt

@@ -1,15 +1,17 @@
 package ch.threema.app.mediaattacher
 
-import android.content.ContentResolver
 import android.content.Context
 import android.net.Uri
-import android.text.format.DateFormat
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import ch.threema.app.files.AppDirectoryProvider
 import ch.threema.app.utils.FileUtil
 import ch.threema.app.utils.VCardExtractor
 import ch.threema.base.utils.getThreemaLogger
+import ch.threema.common.DispatcherProvider
+import ch.threema.common.takeUnlessEmpty
 import ezvcard.Ezvcard
 import ezvcard.VCard
 import ezvcard.property.FormattedName
@@ -18,16 +20,20 @@ import ezvcard.property.VCardProperty
 import java.io.BufferedReader
 import java.io.File
 import java.io.InputStreamReader
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 
 private val logger = getThreemaLogger("EditSendContactViewModel")
 
 /**
  * Contains the data needed in the EditSendContactActivity.
  */
-class EditSendContactViewModel : ViewModel() {
+class EditSendContactViewModel(
+    private val appContext: Context,
+    private val vCardExtractor: VCardExtractor,
+    private val appDirectoryProvider: AppDirectoryProvider,
+    private val dispatcherProvider: DispatcherProvider,
+) : ViewModel() {
     /* The currently shown formatted name in the edit texts */
     private val formattedName: MutableLiveData<FormattedName> = MutableLiveData()
 
@@ -67,53 +73,50 @@ class EditSendContactViewModel : ViewModel() {
     /**
      * Initializes the view model based on the given contact uri.
      */
-    @Suppress("BlockingMethodInNonBlockingContext")
-    fun initializeContact(
-        contactUri: Uri,
-        contentResolver: ContentResolver,
-        extractor: VCardExtractor,
-    ) {
+    fun initializeContact(contactUri: Uri) {
         if (properties.value != null) {
             return
         }
 
-        CoroutineScope(Dispatchers.IO).launch {
-            val vCard = contentResolver.openInputStream(contactUri).use {
-                BufferedReader(InputStreamReader(it)).useLines { l -> l.joinToString("\n") }
-            }.let {
-                Ezvcard.parse(it).first()
-            }
+        viewModelScope.launch {
+            val vCard = readVCard(contactUri)
 
-            if (createFormattedName(
-                    vCard,
-                    extractor,
-                ) == "" &&
-                vCard.formattedName?.value ?: "" != ""
-            ) {
+            if (vCard == null) {
+                logger.warn("vCard was null")
+                structuredName.postValue(StructuredName())
+            } else if (createFormattedName(vCard) == null && !vCard.formattedName?.value.isNullOrEmpty()) {
                 formattedName.postValue(vCard.formattedName)
             } else {
                 structuredName.postValue(vCard.structuredName ?: StructuredName())
             }
 
-            properties.postValue(vCard.properties.associateWith { true }.toMutableMap())
+            properties.postValue(
+                vCard?.properties?.associateWith { true }?.toMutableMap() ?: mutableMapOf(),
+            )
         }
     }
 
+    private suspend fun readVCard(contactUri: Uri): VCard? = withContext(dispatcherProvider.io) {
+        appContext.contentResolver.openInputStream(contactUri)
+            .use { inputStream ->
+                BufferedReader(InputStreamReader(inputStream)).useLines { l -> l.joinToString("\n") }
+            }
+            .let { vCardString ->
+                Ezvcard.parse(vCardString).first()
+            }
+    }
+
     /**
      * Get the formatted name and the vCard as file containing the selected properties.
      */
-    @Suppress("BlockingMethodInNonBlockingContext")
-    fun prepareFinalVCard(context: Context, cacheDir: File, contactUri: Uri) {
-        CoroutineScope(Dispatchers.IO).launch {
+    fun prepareFinalVCard(contactUri: Uri) {
+        viewModelScope.launch(dispatcherProvider.io) {
             val vCard = VCard()
             if (structuredName.value != null) {
                 vCard.setProperty(structuredName.value)
                 vCard.setProperty(
                     FormattedName(
-                        createFormattedName(
-                            vCard,
-                            VCardExtractor(DateFormat.getDateFormat(context), context.resources),
-                        ),
+                        createFormattedName(vCard) ?: "",
                     ),
                 )
             } else if (formattedName.value != null) {
@@ -125,8 +128,8 @@ class EditSendContactViewModel : ViewModel() {
                 vCard.addProperty(it)
             }
 
-            val mimeType = FileUtil.getMimeTypeFromUri(context, contactUri)
-            val modifiedContactFile = File(cacheDir, FileUtil.getDefaultFilename(mimeType))
+            val mimeType = FileUtil.getMimeTypeFromUri(appContext, contactUri)
+            val modifiedContactFile = File(appDirectoryProvider.cacheDirectory, FileUtil.getDefaultFilename(mimeType))
 
             val writer = Ezvcard.write(vCard).prodId(false)
             writer.go(modifiedContactFile)
@@ -138,16 +141,18 @@ class EditSendContactViewModel : ViewModel() {
     /**
      * Create the formatted name (FN) based on the structured name (N).
      */
-    private fun createFormattedName(vcard: VCard, extractor: VCardExtractor): String {
-        if (vcard.structuredName != null) {
-            try {
-                return extractor.getText(vcard.structuredName, false).trim()
-            } catch (e: Exception) {
-                if (e !is VCardExtractor.VCardExtractionException) {
-                    logger.error("Could not extract name of contact", e)
+    private fun createFormattedName(vcard: VCard): String? =
+        vcard.structuredName
+            ?.let { structuredName ->
+                try {
+                    vCardExtractor.getText(structuredName, false)
+                        .trim()
+                        .takeUnlessEmpty()
+                } catch (e: Exception) {
+                    if (e !is VCardExtractor.VCardExtractionException) {
+                        logger.error("Could not extract name of contact", e)
+                    }
+                    null
                 }
             }
-        }
-        return ""
-    }
 }

+ 13 - 1
app/src/main/java/ch/threema/app/mediaattacher/MediaAttacherFeatureModule.kt

@@ -1,9 +1,21 @@
 package ch.threema.app.mediaattacher
 
+import android.content.Context
+import android.text.format.DateFormat
+import ch.threema.app.utils.VCardExtractor
+import org.koin.core.module.dsl.viewModel
 import org.koin.core.module.dsl.viewModelOf
 import org.koin.dsl.module
 
 val mediaAttacherFeatureModule = module {
-    viewModelOf(::EditSendContactViewModel)
+    viewModel<EditSendContactViewModel> {
+        val appContext = get<Context>()
+        EditSendContactViewModel(
+            appContext = appContext,
+            appDirectoryProvider = get(),
+            vCardExtractor = VCardExtractor(DateFormat.getDateFormat(appContext), appContext.resources),
+            dispatcherProvider = get(),
+        )
+    }
     viewModelOf(::MediaAttachViewModel)
 }

+ 2 - 3
app/src/main/java/ch/threema/app/notifications/ConversationNotificationConverter.kt

@@ -20,8 +20,8 @@ class ConversationNotificationConverter(
     private val groupService: GroupService? by injectNullableNonBinding()
     private val conversationCategoryService: ConversationCategoryService? by injectNullableNonBinding()
 
-    fun convert(message: AbstractMessageModel): ConversationNotification? {
-        return ConversationNotificationUtil.convert(
+    fun convert(message: AbstractMessageModel): ConversationNotification? =
+        ConversationNotificationUtil.convert(
             appContext,
             message,
             contactService ?: return null,
@@ -29,5 +29,4 @@ class ConversationNotificationConverter(
             conversationCategoryService ?: return null,
             preferenceService.getContactNameFormat(),
         )
-    }
 }

+ 0 - 2
app/src/main/java/ch/threema/app/preference/SettingsAppearanceFragment.kt

@@ -50,8 +50,6 @@ class SettingsAppearanceFragment : ThreemaPreferenceFragment() {
     )
 
     override fun initializePreferences() {
-        super.initializePreferences()
-
         this.showBadge = getPrefOrNull(R.string.preferences__show_unread_badge)
         this.showBadgeChecked = this.showBadge?.isChecked ?: false
 

+ 0 - 1
app/src/main/java/ch/threema/app/preference/SettingsCallsFragment.kt

@@ -41,7 +41,6 @@ class SettingsCallsFragment : ThreemaPreferenceFragment() {
     private lateinit var groupCallsEnable: CheckBoxPreference
 
     override fun initializePreferences() {
-        super.initializePreferences()
         initCallPrefListeners()
         initWorkRestrictions()
         initEnableCallRejectPref()

+ 0 - 5
app/src/main/java/ch/threema/app/preference/SettingsMediaFragment.kt

@@ -27,14 +27,9 @@ class SettingsMediaFragment : ThreemaPreferenceFragment() {
     private val appRestrictions: AppRestrictions by inject()
 
     override fun initializePreferences() {
-        super.initializePreferences()
-
         initAutoDownloadPref()
-
         initMediaPref()
-
         initSaveMediaPref()
-
         initDownloadPref()
     }
 

+ 0 - 2
app/src/main/java/ch/threema/app/preference/SettingsPrivacyFragment.kt

@@ -99,8 +99,6 @@ class SettingsPrivacyFragment :
         }
 
     override fun initializePreferences() {
-        super.initializePreferences()
-
         disableScreenshot = getPref(synchronizedSettingsService.getScreenshotPolicySetting().preferenceKey)
         disableScreenshotChecked = this.disableScreenshot.isChecked
 

+ 15 - 10
app/src/main/java/ch/threema/app/preference/SettingsSummaryFragment.kt

@@ -30,6 +30,7 @@ import ch.threema.app.asynctasks.ContactCreated
 import ch.threema.app.asynctasks.ContactModified
 import ch.threema.app.asynctasks.ContactResult
 import ch.threema.app.backupcenter.BackupCenterActivity
+import ch.threema.app.di.awaitAppFullyReady
 import ch.threema.app.dialogs.BottomSheetAbstractDialog.BottomSheetDialogCallback
 import ch.threema.app.dialogs.BottomSheetGridDialog
 import ch.threema.app.dialogs.GenericAlertDialog
@@ -98,16 +99,20 @@ class SettingsSummaryFragment : ThreemaPreferenceFragment(), GenericAlertDialog.
             getPref<Preference>(prefKey).summary = summary
         }
 
-        setUpAppSettingsSection()
-        setUpBackupCenter()
-        setUpLinkedDevicesSection()
-        setUpCommunityAndSharingSection()
-        setUpAdvancedSection()
-        setUpPromotionalSection()
-        setUpReferralBanner()
-
-        if (ConfigUtils.isTabletLayout()) {
-            selectActivePreference()
+        lifecycleScope.launch {
+            awaitAppFullyReady()
+
+            setUpAppSettingsSection()
+            setUpBackupCenter()
+            setUpLinkedDevicesSection()
+            setUpCommunityAndSharingSection()
+            setUpAdvancedSection()
+            setUpPromotionalSection()
+            setUpReferralBanner()
+
+            if (ConfigUtils.isTabletLayout()) {
+                selectActivePreference()
+            }
         }
     }
 

+ 10 - 0
app/src/main/java/ch/threema/app/services/AudioPlayerService.kt

@@ -1,5 +1,6 @@
 package ch.threema.app.services
 
+import android.content.Intent
 import androidx.media3.common.AudioAttributes
 import androidx.media3.common.C
 import androidx.media3.common.C.AudioContentType
@@ -79,6 +80,15 @@ class AudioPlayerService : MediaSessionService() {
         logger.info("Created media session")
     }
 
+    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
+        try {
+            super.onStartCommand(intent, flags, startId)
+        } catch (e: Exception) {
+            logger.error("Foreground service start did not work, stopping self", e)
+            stopSelf()
+            START_NOT_STICKY
+        }
+
     override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
         // Only allow media controllers from our own app to connect to this media session
         if (!controllerInfo.isPackageNameVerified || controllerInfo.packageName != this.packageName || !controllerInfo.isTrusted) {

+ 6 - 1
app/src/main/java/ch/threema/app/services/notification/NotificationServiceImpl.kt

@@ -11,6 +11,7 @@ import android.content.Context
 import android.content.Intent
 import android.media.AudioManager
 import android.os.Build
+import android.os.DeadObjectException
 import android.provider.Settings
 import androidx.core.app.NotificationCompat
 import androidx.core.app.NotificationManagerCompat
@@ -1298,7 +1299,11 @@ class NotificationServiceImpl(
     }
 
     private fun cancel(notificationId: Int, tag: String? = null) {
-        notificationManagerCompat.cancel(tag, notificationId)
+        try {
+            notificationManagerCompat.cancel(tag, notificationId)
+        } catch (e: DeadObjectException) {
+            logger.error("Failed to cancel notification", e)
+        }
     }
 
     companion object {

+ 14 - 5
app/src/main/java/ch/threema/app/shortcuts/ShortcutsUpdaterMonitor.kt

@@ -1,5 +1,6 @@
 package ch.threema.app.shortcuts
 
+import android.os.DeadObjectException
 import ch.threema.app.di.injectNullableNonBinding
 import ch.threema.app.eventbus.GlobalEventFlows
 import ch.threema.app.eventbus.events.ContactEvent
@@ -12,6 +13,7 @@ import ch.threema.app.services.ContactService
 import ch.threema.app.services.DistributionListService
 import ch.threema.app.services.GroupService
 import ch.threema.app.utils.ShortcutUtil
+import ch.threema.base.utils.getThreemaLogger
 import ch.threema.data.datatypes.GroupIdentity
 import ch.threema.domain.types.Identity
 import ch.threema.storage.models.AbstractMessageModel
@@ -20,12 +22,14 @@ import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
 import org.koin.core.component.KoinComponent
 
+private val logger = getThreemaLogger("ShortcutsUpdaterMonitor")
+
 // TODO(ANDR-4705): Refactor or wrap ShortcutUtil to make it injectable and ensure that updatePinnedShortcut is only called from here.
 //  Once that is done, we should write unit tests for this class.
 class ShortcutsUpdaterMonitor(
     private val globalEventFlows: GlobalEventFlows,
     private val preferenceService: PreferenceService,
-) : Monitor("ShortcutsMonitor"), KoinComponent {
+) : Monitor("ShortcutsUpdaterMonitor"), KoinComponent {
     private val contactService: ContactService? by injectNullableNonBinding()
     private val groupService: GroupService? by injectNullableNonBinding()
     private val distributionListService: DistributionListService? by injectNullableNonBinding()
@@ -101,10 +105,15 @@ class ShortcutsUpdaterMonitor(
     private fun updateShareTargetShortcuts(
         receiver: MessageReceiver<out AbstractMessageModel>,
     ) {
-        ShortcutUtil.updateShareTargetShortcut(
-            receiver,
-            preferenceService.getContactNameFormat(),
-        )
+        try {
+            ShortcutUtil.updateShareTargetShortcut(
+                receiver,
+                preferenceService.getContactNameFormat(),
+            )
+        } catch (e: DeadObjectException) {
+            // DeadObjectException is expected to occur occasionally and there is nothing we can do about it
+            logger.warn("Failed to update shortcuts", e)
+        }
     }
 
     private fun updatePinnedShortcuts(

+ 4 - 0
app/src/main/java/ch/threema/app/startup/AppStartupMonitorImpl.kt

@@ -143,6 +143,10 @@ class AppStartupMonitorImpl : AppStartupMonitor {
      * If an app startup error is reported, this will never return, as the app is never considered "ready" in that case.
      */
     override suspend fun awaitAll() {
+        // We return immediately if the app is already ready, which allows us to avoid a coroutine suspension
+        if (isReady()) {
+            return
+        }
         systemStatuses.first { statuses ->
             statuses.all { (_, status) ->
                 status == SystemStatus.READY

+ 2 - 4
app/src/main/java/ch/threema/app/startup/components/ErrorState.kt

@@ -65,8 +65,8 @@ private fun UnexpectedErrorState(
     onClickExportLogs: () -> Unit,
 ) {
     ErrorState(
-        message = stringResource(R.string.an_error_occurred),
-        details = errorCodes.joinToString(),
+        message = stringResource(R.string.an_error_occurred_more, errorCodes.joinToString()),
+        details = stringResource(R.string.startup_error_details),
     ) {
         ButtonPrimary(
             modifier = Modifier.fillMaxWidth(),
@@ -164,10 +164,8 @@ fun ErrorState(
             Spacer(modifier = Modifier.height(GridUnit.x2))
             ThemedText(
                 text = details,
-                maxLines = 2,
                 textAlign = TextAlign.Center,
                 style = MaterialTheme.typography.bodyMedium,
-                overflow = TextOverflow.Ellipsis,
                 color = MaterialTheme.colorScheme.onBackground,
             )
         }

+ 3 - 3
app/src/main/java/ch/threema/app/tasks/OnFSFeatureMaskDowngradedTask.kt

@@ -3,6 +3,7 @@ package ch.threema.app.tasks
 import ch.threema.app.services.ContactService
 import ch.threema.app.services.MessageService
 import ch.threema.base.utils.getThreemaLogger
+import ch.threema.data.IdentityProvider
 import ch.threema.data.models.ContactModel
 import ch.threema.data.models.ContactModelData
 import ch.threema.data.repositories.ContactModelRepository
@@ -10,7 +11,6 @@ import ch.threema.domain.fs.DHSession
 import ch.threema.domain.protocol.ThreemaFeature
 import ch.threema.domain.stores.DHSessionStore
 import ch.threema.domain.stores.DHSessionStoreException
-import ch.threema.domain.stores.IdentityStore
 import ch.threema.domain.taskmanager.ActiveTask
 import ch.threema.domain.taskmanager.ActiveTaskCodec
 import ch.threema.domain.taskmanager.Task
@@ -45,7 +45,7 @@ class OnFSFeatureMaskDowngradedTask(
     private val contactModelRepository: ContactModelRepository by inject()
     private val messageService: MessageService by inject()
     private val dhSessionStore: DHSessionStore by inject()
-    private val identityStore: IdentityStore by inject()
+    private val identityProvider: IdentityProvider by inject()
 
     override val type = "FSFeatureMaskDowngraded"
 
@@ -70,7 +70,7 @@ class OnFSFeatureMaskDowngradedTask(
         try {
             fsSession = dhSessionStore.getBestDHSession(
                 /* myIdentity = */
-                identityStore.getIdentityString(),
+                identityProvider.getIdentityString(),
                 /* peerIdentity = */
                 identity,
                 /* handle = */

+ 1 - 1
app/src/main/java/ch/threema/app/threemasafe/BackupThreemaSafeFragment.java

@@ -132,7 +132,7 @@ public class BackupThreemaSafeFragment extends Fragment implements GenericAlertD
             floatingActionButton = fragmentView.findViewById(R.id.floating);
             floatingActionButton.setOnClickListener(v -> {
                 if (preferenceService.getThreemaSafeEnabled()) {
-                    threemaSafeService.uploadNow(true);
+                    threemaSafeService.uploadNow();
                     threemaSafeService.reschedulePeriodicUpload();
                 }
             });

+ 1 - 1
app/src/main/java/ch/threema/app/threemasafe/ThreemaSafeConfigureActivity.java

@@ -238,7 +238,7 @@ public class ThreemaSafeConfigureActivity extends ThreemaToolbarActivity impleme
         threemaSafeService.storeMasterKey(masterkey);
         dependencies.getPreferenceService().setThreemaSafeServerInfo(serverInfo);
         threemaSafeService.setEnabled(true);
-        threemaSafeService.uploadNow(true);
+        threemaSafeService.uploadNow();
 
         if (updatePasswordOnly) {
             Toast.makeText(this, R.string.safe_password_updated, Toast.LENGTH_LONG).show();

+ 1 - 5
app/src/main/java/ch/threema/app/threemasafe/ThreemaSafeService.java

@@ -102,12 +102,8 @@ public interface ThreemaSafeService {
      * Create a one time work request to create and upload a Threema Safe backup. This method
      * cancels already enqueued one time work requests. If a periodic work request is scheduled, it
      * gets rescheduled, so that the schedule period is still met.
-     *
-     * @param force If set to {@code true}, the backup is created even if the last backup was
-     *              created within the last 24 hours or there are no new changes since the last
-     *              backup. See {@link #createBackup(boolean)} for more details.
      */
-    void uploadNow(boolean force);
+    void uploadNow();
 
     /**
      * Create and upload a Threema Safe backup.

+ 7 - 32
app/src/main/java/ch/threema/app/threemasafe/ThreemaSafeServiceImpl.java

@@ -4,12 +4,8 @@ import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
-import android.hardware.display.DisplayManager;
 import android.text.TextUtils;
 import android.text.format.DateUtils;
-import android.view.Display;
-import android.view.WindowManager;
-import android.widget.Toast;
 
 import ch.threema.app.preference.service.SynchronizedSettingsService;
 import ch.threema.app.protocolsteps.ValidContactsLookupSteps;
@@ -53,7 +49,6 @@ import androidx.work.PeriodicWorkRequest;
 import androidx.work.WorkManager;
 import ch.threema.app.BuildConfig;
 import ch.threema.app.R;
-import ch.threema.app.ThreemaApplication;
 import ch.threema.app.asynctasks.AddContactRestrictionPolicy;
 import ch.threema.app.asynctasks.BasicAddOrUpdateContactBackgroundTask;
 import ch.threema.app.asynctasks.ContactAvailable;
@@ -73,7 +68,6 @@ import ch.threema.app.services.UserService;
 import ch.threema.app.utils.BitmapUtil;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.GzipOutputStream;
-import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.WorkManagerUtil;
 import ch.threema.app.workers.ThreemaSafeUploadWorker;
 import ch.threema.app.workers.WorkerNames;
@@ -113,7 +107,6 @@ import ch.threema.storage.models.group.GroupMemberModel;
 import ch.threema.storage.models.group.GroupModelOld;
 import okhttp3.OkHttpClient;
 
-import static android.view.Display.DEFAULT_DISPLAY;
 import static ch.threema.app.preference.service.PreferenceService.PROFILEPIC_RELEASE_EVERYONE;
 import static ch.threema.app.preference.service.PreferenceService.PROFILEPIC_RELEASE_NOBODY;
 import static ch.threema.app.preference.service.PreferenceService.PROFILEPIC_RELEASE_ALLOW_LIST;
@@ -382,7 +375,7 @@ public class ThreemaSafeServiceImpl implements ThreemaSafeService {
     @Override
     public boolean schedulePeriodicUpload() {
         logger.info("Scheduling Threema Safe upload");
-        WorkManager workManager = WorkManager.getInstance(ThreemaApplication.getAppContext());
+        WorkManager workManager = WorkManager.getInstance(context);
         boolean reschedule = WorkManagerUtil.shouldScheduleNewWorkManagerInstance(
             workManager,
             WorkerNames.WORKER_PERIODIC_THREEMA_SAFE_UPLOAD,
@@ -401,7 +394,7 @@ public class ThreemaSafeServiceImpl implements ThreemaSafeService {
     public void unschedulePeriodicUpload() {
         logger.info("Unscheduling Threema Safe upload");
 
-        WorkManager workManager = WorkManager.getInstance(ThreemaApplication.getAppContext());
+        WorkManager workManager = WorkManager.getInstance(context);
         workManager.cancelUniqueWork(WorkerNames.WORKER_PERIODIC_THREEMA_SAFE_UPLOAD);
     }
 
@@ -428,9 +421,9 @@ public class ThreemaSafeServiceImpl implements ThreemaSafeService {
     }
 
     @Override
-    public void uploadNow(boolean force) {
+    public void uploadNow() {
         try {
-            WorkManager workManager = WorkManager.getInstance(ThreemaApplication.getAppContext());
+            WorkManager workManager = WorkManager.getInstance(context);
 
             // If the periodic safe upload is scheduled, we can reschedule it so that it does not
             // get executed within the next schedule period.
@@ -439,7 +432,7 @@ public class ThreemaSafeServiceImpl implements ThreemaSafeService {
             }
 
             // Upload the threema safe once
-            OneTimeWorkRequest workRequest = ThreemaSafeUploadWorker.buildWorkRequest(force);
+            OneTimeWorkRequest workRequest = ThreemaSafeUploadWorker.buildRunNowWorkRequest();
             workManager.enqueueUniqueWork(WorkerNames.WORKER_THREEMA_SAFE_UPLOAD, ExistingWorkPolicy.REPLACE, workRequest);
         } catch (IllegalStateException e) {
             logger.error("Unable to schedule safe upload one time work", e);
@@ -544,24 +537,6 @@ public class ThreemaSafeServiceImpl implements ThreemaSafeService {
             throw new ThreemaSafeUploadException("Upload failed", true);
         }
 
-        if (force) {
-            RuntimeUtil.runOnUiThread(() -> {
-                Context windowContext = context;
-                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
-                    final DisplayManager dm;
-                    dm = context.getSystemService(DisplayManager.class);
-                    final Display primaryDisplay = dm.getDisplay(DEFAULT_DISPLAY);
-                    try {
-                        windowContext = context.createDisplayContext(primaryDisplay)
-                            .createWindowContext(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, null);
-                    } catch (SecurityException e) {
-                        logger.error("Unable to create WindowContext for Toast", e);
-                    }
-                }
-                Toast.makeText(windowContext, R.string.threema_safe_upload_successful, Toast.LENGTH_LONG).show();
-            });
-        }
-
         // This is only required as for some users the default server name has been stored in the
         // preferences. Default server names should not be stored in preferences as they prevent us
         // from changing the default server name easily.
@@ -637,8 +612,8 @@ public class ThreemaSafeServiceImpl implements ThreemaSafeService {
 
             try {
                 // Schedule the start of the worker every 24 hours
-                WorkManager workManager = WorkManager.getInstance(ThreemaApplication.getAppContext());
-                PeriodicWorkRequest workRequest = ThreemaSafeUploadWorker.buildWorkRequest(SCHEDULE_PERIOD);
+                WorkManager workManager = WorkManager.getInstance(context);
+                PeriodicWorkRequest workRequest = ThreemaSafeUploadWorker.buildPeriodicWorkRequest(SCHEDULE_PERIOD);
                 workManager.enqueueUniquePeriodicWork(
                     WorkerNames.WORKER_PERIODIC_THREEMA_SAFE_UPLOAD,
                     policy,

+ 11 - 2
app/src/main/java/ch/threema/app/utils/ConversationNotificationUtil.java

@@ -49,6 +49,7 @@ public class ConversationNotificationUtil {
     protected static final HashMap<String, ConversationNotificationGroup> notificationGroupHashMap = new HashMap<>();
     private static final int MAX_NOTIFICATION_THUMBNAIL_SIZE_BYTES = 1024 * 1024;
 
+    @Nullable
     public static ConversationNotification convert(
         @NonNull Context context,
         @NonNull AbstractMessageModel messageModel,
@@ -127,7 +128,7 @@ public class ConversationNotificationUtil {
         return messageModel.getCreatedAt();
     }
 
-    @NonNull
+    @Nullable
     private static ConversationNotification create(
         @NonNull final Context context,
         @NonNull final MessageModel messageModel,
@@ -136,6 +137,10 @@ public class ConversationNotificationUtil {
         @NonNull final ContactNameFormat contactNameFormat
     ) {
         final @Nullable ContactModel contactModel = contactService.getByIdentity(messageModel.getIdentity());
+        if (contactModel == null) {
+            logger.warn("Contact for notification not found");
+            return null;
+        }
         final String groupUid = "i" + messageModel.getIdentity();
         synchronized (notificationGroupHashMap) {
             ConversationNotificationGroup group = notificationGroupHashMap.get(groupUid);
@@ -201,7 +206,7 @@ public class ConversationNotificationUtil {
         return null;
     }
 
-    @NonNull
+    @Nullable
     private static ConversationNotification create(
         final Context context,
         @NonNull final GroupMessageModel messageModel,
@@ -210,6 +215,10 @@ public class ConversationNotificationUtil {
         @NonNull final ContactNameFormat contactNameFormat
     ) {
         final GroupModelOld groupModel = groupService.getById(messageModel.getGroupId());
+        if (groupModel == null) {
+            logger.warn("Group for notification not found");
+            return null;
+        }
         final String groupUid = "g" + messageModel.getGroupId();
         synchronized (notificationGroupHashMap) {
             @Nullable ConversationNotificationGroup group = notificationGroupHashMap.get(groupUid);

+ 22 - 11
app/src/main/java/ch/threema/app/voicemessage/VoiceRecorderViewModel.kt

@@ -29,6 +29,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.isActive
 import kotlinx.coroutines.launch
 
@@ -197,7 +198,7 @@ class VoiceRecorderViewModel(
             mediaRecorder?.let { recorder ->
                 recorder.stop()
                 logger.info("Stopped recording to {} with {}", audioOutputFile, recorder)
-                if (audioOutputFile?.exists() != true) {
+                if (audioOutputFile != null && !audioOutputFile.exists()) {
                     logger.logAndReportError("Output file does not exist after stopping recording")
                 }
             }
@@ -205,11 +206,15 @@ class VoiceRecorderViewModel(
             logger.error("Exception while stopping recording", e)
         }
         releaseMediaRecorder()
-        _state.value = _state.value.copy(
-            mediaState = MediaState.FinishedRecording(
-                file = audioOutputFile!!,
-            ),
-        )
+        if (audioOutputFile != null) {
+            _state.update { previousState ->
+                previousState.copy(
+                    mediaState = MediaState.FinishedRecording(
+                        file = audioOutputFile,
+                    ),
+                )
+            }
+        }
     }
 
     fun startPlayback() {
@@ -415,11 +420,17 @@ class VoiceRecorderViewModel(
             logger.error("File was not found")
             return Duration.ZERO
         }
-        val durationCheckMediaPlayer: MediaPlayer = MediaPlayer.create(appContext, file.toUri())
-            ?: run {
-                logger.warn("Unable to create a media player for checking size")
-                return Duration.ZERO
-            }
+        val durationCheckMediaPlayer: MediaPlayer = try {
+            MediaPlayer.create(appContext, file.toUri())
+                ?: run {
+                    logger.warn("Unable to create a media player for checking size")
+                    return Duration.ZERO
+                }
+        } catch (e: IllegalStateException) {
+            logger.error("Failed to create media player to check duration", e)
+            return Duration.ZERO
+        }
+
         val durationMs = durationCheckMediaPlayer.duration
         durationCheckMediaPlayer.release()
         logger.info("Duration in ms {}", durationMs)

+ 13 - 6
app/src/main/java/ch/threema/app/voip/groupcall/service/GroupCallControllerImpl.kt

@@ -14,7 +14,6 @@ import ch.threema.app.voip.groupcall.sfu.connection.GroupCallConnectionState
 import ch.threema.app.voip.groupcall.sfu.connection.Joining
 import ch.threema.app.voip.groupcall.sfu.messages.P2PMessageContent
 import ch.threema.app.voip.groupcall.sfu.webrtc.RemoteCtx
-import ch.threema.base.ThreemaException
 import ch.threema.base.utils.getThreemaLogger
 import ch.threema.domain.types.Identity
 import java.lang.Runnable
@@ -26,6 +25,8 @@ import kotlinx.coroutines.channels.BufferOverflow
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flowOf
 import org.webrtc.EglBase
 
 private val logger = getThreemaLogger("GroupCallControllerImpl")
@@ -101,18 +102,24 @@ internal class GroupCallControllerImpl(
     private lateinit var localParticipant: LocalParticipant
 
     override val participants: Flow<Set<Participant>>
-        get() = ifCallIsRunning {
+        get() = if (callLeftSignal.isCompleted) {
+            flowOf(emptySet())
+        } else {
             logger.trace("Get participants")
             mutableParticipants.asSharedFlow()
         }
 
     override val captureStateUpdates: Flow<Unit>
-        get() = ifCallIsRunning {
+        get() = if (callLeftSignal.isCompleted) {
+            emptyFlow()
+        } else {
             mutableUpdateCaptureState.asSharedFlow()
         }
 
     override val screenShareActivated: Flow<Unit>
-        get() = ifCallIsRunning {
+        get() = if (callLeftSignal.isCompleted) {
+            emptyFlow()
+        } else {
             mutableScreenShareActivated.asSharedFlow()
         }
 
@@ -339,7 +346,7 @@ internal class GroupCallControllerImpl(
     @AnyThread
     private fun <T> ifCallIsRunning(supplier: () -> T): T {
         return if (callLeftSignal.isCompleted) {
-            throw ThreemaException("Call has already ended")
+            throw GroupCallController.CallAlreadyEndedException()
         } else {
             supplier()
         }
@@ -348,7 +355,7 @@ internal class GroupCallControllerImpl(
     @AnyThread
     private suspend fun <T> suspendingIfCallIsRunning(supplier: suspend () -> T): T {
         return if (callLeftSignal.isCompleted) {
-            throw ThreemaException("Call has already ended")
+            throw GroupCallController.CallAlreadyEndedException()
         } else {
             supplier()
         }

+ 2 - 0
app/src/main/java/ch/threema/app/voip/groupcall/sfu/GroupCallController.kt

@@ -104,4 +104,6 @@ interface GroupCallController {
      */
     @WorkerThread
     fun purgeCallParticipants(groupMembers: Set<String>)
+
+    class CallAlreadyEndedException : Exception()
 }

+ 18 - 9
app/src/main/java/ch/threema/app/voip/viewmodel/GroupCallViewModel.kt

@@ -255,16 +255,24 @@ class GroupCallViewModel(
 
     @UiThread
     fun muteMicrophone(muted: Boolean) {
-        logger.trace("Mute {}", muted)
-        callController.microphoneActive = !muted
-        microphoneActive.postValue(callController.microphoneActive)
+        try {
+            logger.trace("Mute {}", muted)
+            callController.microphoneActive = !muted
+            microphoneActive.postValue(callController.microphoneActive)
+        } catch (_: GroupCallController.CallAlreadyEndedException) {
+            return
+        }
         triggerCaptureStateUpdate()
     }
 
     @UiThread
     fun muteCamera(muted: Boolean) {
-        callController.cameraActive = !muted
-        cameraActive.postValue(callController.cameraActive)
+        try {
+            callController.cameraActive = !muted
+            cameraActive.postValue(callController.cameraActive)
+        } catch (_: GroupCallController.CallAlreadyEndedException) {
+            return
+        }
         triggerCaptureStateUpdate()
         // If camera is turned on, then don't use earpiece as output anymore as it is not convenient
         if (!muted && selectedAudioDevice.value == AudioDevice.EARPIECE) {
@@ -273,12 +281,13 @@ class GroupCallViewModel(
         }
     }
 
-    @UiThread
-    fun flipCamera() {
-        viewModelScope.launch {
+    fun flipCamera() = viewModelScope.launch {
+        try {
             callController.flipCamera()
-            cameraFlipEvents.postValue(Unit)
+        } catch (_: GroupCallController.CallAlreadyEndedException) {
+            return@launch
         }
+        cameraFlipEvents.postValue(Unit)
     }
 
     fun hasOtherJoinedCall(call: GroupCallDescription): Boolean {

+ 13 - 4
app/src/main/java/ch/threema/app/webviews/PrivacyPolicyActivity.kt

@@ -1,6 +1,8 @@
 package ch.threema.app.webviews
 
 import android.content.Context
+import android.os.Bundle
+import androidx.core.view.isInvisible
 import ch.threema.android.buildActivityIntent
 import ch.threema.app.R
 import ch.threema.app.utils.ConfigUtils
@@ -14,6 +16,13 @@ class PrivacyPolicyActivity : SimpleWebViewActivity() {
         logScreenVisibility(logger)
     }
 
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        if (intent.extras?.getBoolean(HIDE_CONNECTION_INDICATOR, false) == true) {
+            connectionIndicator?.isInvisible = true
+        }
+    }
+
     override fun getWebViewTitle() = R.string.privacy_policy
 
     override fun getWebViewUrl(isDarkTheme: Boolean) = ConfigUtils.getPrivacyPolicyURL(
@@ -24,11 +33,11 @@ class PrivacyPolicyActivity : SimpleWebViewActivity() {
     )
 
     companion object {
+        private const val HIDE_CONNECTION_INDICATOR = "hideConnectionIndicator"
+
         @JvmStatic
-        fun createIntent(context: Context, forceDarkTheme: Boolean = false) = buildActivityIntent<PrivacyPolicyActivity>(context) {
-            if (forceDarkTheme) {
-                putExtra(FORCE_DARK_THEME, true)
-            }
+        fun createIntent(context: Context, hideConnectionIndicator: Boolean = false) = buildActivityIntent<PrivacyPolicyActivity>(context) {
+            putExtra(HIDE_CONNECTION_INDICATOR, hideConnectionIndicator)
         }
     }
 }

+ 0 - 141
app/src/main/java/ch/threema/app/webviews/SimpleWebViewActivity.java

@@ -1,141 +0,0 @@
-package ch.threema.app.webviews;
-
-import android.content.Intent;
-import android.net.ConnectivityManager;
-import android.net.Uri;
-import android.os.Bundle;
-import android.view.View;
-import android.webkit.WebChromeClient;
-import android.webkit.WebView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.StringRes;
-import androidx.appcompat.app.AppCompatDelegate;
-
-import com.google.android.material.appbar.MaterialToolbar;
-import com.google.android.material.progressindicator.LinearProgressIndicator;
-
-import ch.threema.app.R;
-import ch.threema.app.activities.ThreemaToolbarActivity;
-import ch.threema.app.dialogs.GenericAlertDialog;
-import ch.threema.app.ui.InsetSides;
-import ch.threema.app.ui.ViewExtensionsKt;
-import ch.threema.app.utils.ConfigUtils;
-
-/**
- * Warning! Do not start an Activity extending this class from an application context!
- */
-public abstract class SimpleWebViewActivity extends ThreemaToolbarActivity implements GenericAlertDialog.DialogClickListener {
-
-    public static final String FORCE_DARK_THEME = "darkTheme";
-    private static final String DIALOG_TAG_NO_CONNECTION = "nc";
-    private LinearProgressIndicator progressBar;
-    private WebView webView;
-
-    private boolean isDarkThemeForced = false;
-
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        MaterialToolbar toolbar = findViewById(R.id.material_toolbar);
-        toolbar.setNavigationOnClickListener(view -> finish());
-        toolbar.setTitle(getWebViewTitle());
-
-        Intent intent = getIntent();
-        final @Nullable Bundle extras = intent.getExtras();
-
-        if (extras != null && extras.getBoolean(FORCE_DARK_THEME, false)) {
-            isDarkThemeForced = true;
-            if (getConnectionIndicator() != null) {
-                // hide connection indicator when dark theme is forced
-                getConnectionIndicator().setVisibility(View.INVISIBLE);
-            }
-        }
-
-        if (!ConfigUtils.isTheDarkSide(this) && isDarkThemeForced) {
-            getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
-        }
-
-        progressBar = findViewById(R.id.progress);
-        webView = findViewById(R.id.simple_webview);
-        webView.getSettings().setJavaScriptEnabled(requiresJavaScript());
-        webView.setWebViewClient(new ThreemaWebViewClient(this::onNavigationRequest));
-
-        if (requiresConnection()) {
-            webView.setWebChromeClient(new WebChromeClient() {
-                @Override
-                public void onProgressChanged(WebView view, int newProgress) {
-                    if (newProgress >= 99) {
-                        progressBar.setVisibility(View.INVISIBLE);
-                    } else {
-                        progressBar.setProgress(newProgress);
-                    }
-                }
-            });
-            checkConnection();
-        } else {
-            progressBar.setVisibility(View.GONE);
-
-            loadWebView();
-        }
-    }
-
-    @Override
-    protected void handleDeviceInsets() {
-        super.handleDeviceInsets();
-        ViewExtensionsKt.applyDeviceInsetsAsPadding(
-            findViewById(R.id.webview_scroller),
-            InsetSides.lbr()
-        );
-    }
-
-    private boolean onNavigationRequest(@NonNull Uri url) {
-        // We don't want to allow navigating to other pages within the web view,
-        // so instead we open the URL in the external browser
-        startActivity(new Intent(Intent.ACTION_VIEW, url));
-        return true;
-    }
-
-    private void loadWebView() {
-        final boolean isDarkTheme = ConfigUtils.isTheDarkSide(this) || isDarkThemeForced;
-        webView.loadUrl(getWebViewUrl(isDarkTheme));
-    }
-
-    private void checkConnection() {
-        ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
-        if (connectivityManager != null && connectivityManager.getActiveNetworkInfo() != null && connectivityManager.getActiveNetworkInfo().isConnected()) {
-            loadWebView();
-        } else {
-            GenericAlertDialog.newInstance(getWebViewTitle(), R.string.internet_connection_required, R.string.retry, R.string.cancel).show(getSupportFragmentManager(), DIALOG_TAG_NO_CONNECTION);
-        }
-    }
-
-    public int getLayoutResource() {
-        return R.layout.activity_simple_webview;
-    }
-
-    @Override
-    public void onYes(@Nullable String tag, @Nullable Object data) {
-        checkConnection();
-    }
-
-    @Override
-    public void onNo(@Nullable String tag, @Nullable Object data) {
-        finish();
-    }
-
-    protected abstract @StringRes int getWebViewTitle();
-
-    @NonNull
-    protected abstract String getWebViewUrl(boolean isDarkTheme);
-
-    protected boolean requiresConnection() {
-        return true;
-    }
-
-    protected boolean requiresJavaScript() {
-        return false;
-    }
-}
-

+ 119 - 0
app/src/main/java/ch/threema/app/webviews/SimpleWebViewActivity.kt

@@ -0,0 +1,119 @@
+package ch.threema.app.webviews
+
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.net.ConnectivityManager
+import android.net.Uri
+import android.os.Bundle
+import android.view.View
+import android.webkit.WebChromeClient
+import android.webkit.WebView
+import androidx.annotation.StringRes
+import androidx.core.content.getSystemService
+import androidx.core.view.isInvisible
+import androidx.core.view.isVisible
+import ch.threema.android.showToast
+import ch.threema.app.R
+import ch.threema.app.activities.ThreemaToolbarActivity
+import ch.threema.app.dialogs.GenericAlertDialog
+import ch.threema.app.dialogs.GenericAlertDialog.DialogClickListener
+import ch.threema.app.ui.InsetSides.Companion.lbr
+import ch.threema.app.ui.applyDeviceInsetsAsPadding
+import ch.threema.app.utils.ConfigUtils
+import ch.threema.base.utils.getThreemaLogger
+import ch.threema.common.consume
+import com.google.android.material.appbar.MaterialToolbar
+import com.google.android.material.progressindicator.LinearProgressIndicator
+
+private val logger = getThreemaLogger("SimpleWebViewActivity")
+
+abstract class SimpleWebViewActivity : ThreemaToolbarActivity(), DialogClickListener {
+    private lateinit var progressBar: LinearProgressIndicator
+    private lateinit var webView: WebView
+
+    public override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        val toolbar = findViewById<MaterialToolbar>(R.id.material_toolbar)
+        toolbar.setNavigationOnClickListener { finish() }
+        toolbar.setTitle(getWebViewTitle())
+
+        progressBar = findViewById(R.id.progress)
+        webView = findViewById(R.id.simple_webview)
+        webView.getSettings().javaScriptEnabled = requiresJavaScript()
+        webView.setWebViewClient(ThreemaWebViewClient(::onNavigationRequest))
+
+        if (requiresConnection()) {
+            webView.setWebChromeClient(object : WebChromeClient() {
+                override fun onProgressChanged(view: WebView?, newProgress: Int) {
+                    if (newProgress >= 99) {
+                        progressBar.isInvisible = true
+                    } else {
+                        progressBar.progress = newProgress
+                    }
+                }
+            })
+            checkConnection()
+        } else {
+            progressBar.isVisible = false
+            loadWebView()
+        }
+    }
+
+    override fun handleDeviceInsets() {
+        super.handleDeviceInsets()
+        findViewById<View?>(R.id.webview_scroller).applyDeviceInsetsAsPadding(lbr())
+    }
+
+    private fun onNavigationRequest(url: Uri): Boolean = consume {
+        // We don't want to allow navigating to other pages within the web view,
+        // so instead we open the URL in the external browser
+        try {
+            startActivity(Intent(Intent.ACTION_VIEW, url))
+        } catch (_: ActivityNotFoundException) {
+            logger.error("No activity found to open URL (scheme={}, host={})", url.scheme, url.host)
+            showToast(R.string.no_activity_for_intent)
+        }
+    }
+
+    private fun loadWebView() {
+        webView.loadUrl(getWebViewUrl(ConfigUtils.isTheDarkSide(this)))
+    }
+
+    private fun checkConnection() {
+        if (getSystemService<ConnectivityManager>()?.activeNetworkInfo?.isConnected == true) {
+            loadWebView()
+        } else {
+            GenericAlertDialog.newInstance(
+                getWebViewTitle(),
+                R.string.internet_connection_required,
+                R.string.retry,
+                R.string.cancel,
+            )
+                .show(supportFragmentManager, DIALOG_TAG_NO_CONNECTION)
+        }
+    }
+
+    override fun getLayoutResource() = R.layout.activity_simple_webview
+
+    override fun onYes(tag: String?, data: Any?) {
+        checkConnection()
+    }
+
+    override fun onNo(tag: String?, data: Any?) {
+        finish()
+    }
+
+    @StringRes
+    protected abstract fun getWebViewTitle(): Int
+
+    protected abstract fun getWebViewUrl(isDarkTheme: Boolean): String
+
+    protected open fun requiresConnection(): Boolean = true
+
+    protected open fun requiresJavaScript(): Boolean = false
+
+    companion object {
+        private const val DIALOG_TAG_NO_CONNECTION = "nc"
+    }
+}

+ 9 - 4
app/src/main/java/ch/threema/app/workers/ThreemaSafeUploadWorker.kt

@@ -7,6 +7,8 @@ import ch.threema.android.buildPeriodicWorkRequest
 import ch.threema.android.setConstraints
 import ch.threema.android.setInitialDelay
 import ch.threema.android.setInputData
+import ch.threema.android.showToast
+import ch.threema.app.R
 import ch.threema.app.di.awaitAppFullyReadyWithTimeout
 import ch.threema.app.managers.ListenerManager
 import ch.threema.app.preference.service.PreferenceService
@@ -25,7 +27,7 @@ import org.koin.core.component.inject
 private val logger = getThreemaLogger("ThreemaSafeUploadWorker")
 
 class ThreemaSafeUploadWorker(
-    context: Context,
+    private val context: Context,
     workerParameters: WorkerParameters,
 ) : CoroutineWorker(context, workerParameters), KoinComponent {
 
@@ -52,6 +54,9 @@ class ThreemaSafeUploadWorker(
             // When the backup has been successfully uploaded or does not need to be uploaded, then
             // we ignore previous errors.
             preferenceService.setThreemaSafeErrorTimestamp(null)
+            if (forceUpdate) {
+                context.showToast(R.string.threema_safe_upload_successful)
+            }
         } catch (e: ThreemaSafeService.ThreemaSafeUploadException) {
             if (preferenceService.getThreemaSafeErrorTimestamp() == null && e.isUploadNeeded) {
                 preferenceService.setThreemaSafeErrorTimestamp(Instant.now())
@@ -92,10 +97,10 @@ class ThreemaSafeUploadWorker(
          * Build a one time work request without any initial delay.
          */
         @JvmStatic
-        fun buildWorkRequest(forceUpdate: Boolean): OneTimeWorkRequest =
+        fun buildRunNowWorkRequest(): OneTimeWorkRequest =
             buildOneTimeWorkRequest<ThreemaSafeUploadWorker> {
                 setInputData {
-                    putBoolean(EXTRA_FORCE_UPDATE, forceUpdate)
+                    putBoolean(EXTRA_FORCE_UPDATE, true)
                 }
             }
 
@@ -105,7 +110,7 @@ class ThreemaSafeUploadWorker(
          * schedule period is not added as tag, as these period does not change dynamically.
          */
         @JvmStatic
-        fun buildWorkRequest(schedulePeriodMs: Long): PeriodicWorkRequest =
+        fun buildPeriodicWorkRequest(schedulePeriodMs: Long): PeriodicWorkRequest =
             buildPeriodicWorkRequest<ThreemaSafeUploadWorker>(
                 repeatInterval = schedulePeriodMs.milliseconds,
             ) {

BIN
app/src/main/res/drawable-hdpi/ic_chat_bubble_bitmap_24dp.png


BIN
app/src/main/res/drawable-mdpi/ic_chat_bubble_bitmap_24dp.png


BIN
app/src/main/res/drawable-xhdpi/ic_chat_bubble_bitmap_24dp.png


BIN
app/src/main/res/drawable-xxhdpi/ic_chat_bubble_bitmap_24dp.png


+ 34 - 24
app/src/main/res/layout/item_widget.xml

@@ -1,20 +1,24 @@
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/item_layout"
     android:layout_width="match_parent"
-    android:background="@color/md_theme_light_background"
-    android:orientation="horizontal"
     android:layout_height="72dp"
-    android:paddingLeft="16dp"
-    android:paddingRight="16dp">
+    android:background="@color/widget_color_background"
+    android:orientation="horizontal"
+    android:paddingHorizontal="@dimen/grid_unit_x2">
+
+    <!-- Do not use ?attr/ references for colors in this layout file. A remote view can not resolve them -->
 
     <ImageView
         android:id="@+id/avatar"
-        android:visibility="visible"
-        android:layout_gravity="center_vertical"
         android:layout_width="@dimen/avatar_size_small"
         android:layout_height="@dimen/avatar_size_small"
-        android:layout_marginRight="8dp" />
+        android:layout_gravity="center_vertical"
+        android:layout_marginRight="@dimen/grid_unit_x1"
+        android:importantForAccessibility="no"
+        android:visibility="visible"
+        tools:background="@drawable/ic_contact" />
 
     <RelativeLayout
         android:layout_width="match_parent"
@@ -25,50 +29,56 @@
             android:id="@+id/sender_text"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:textColor="@color/md_theme_light_onBackground"
             android:layout_alignParentLeft="true"
             android:layout_toLeftOf="@+id/msg_date"
-            android:textSize="18sp"
-            android:maxLines="1"
+            android:ellipsize="end"
             android:fontFamily="sans-serif-light"
-            android:text="" />
+            android:maxLines="1"
+            android:text=""
+            android:textColor="@color/widget_color_on_background"
+            android:textSize="18sp"
+            tools:text="Sender Name" />
 
         <TextView
             android:id="@+id/msg_date"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_marginLeft="4dp"
-            android:layout_alignParentRight="true"
             android:layout_alignBaseline="@id/sender_text"
-            android:textColor="@color/md_theme_light_onBackground"
-            android:textSize="12sp"
+            android:layout_alignParentRight="true"
+            android:layout_marginLeft="@dimen/grid_unit_x0_5"
             android:maxLines="1"
-            android:text="" />
+            android:text=""
+            android:textColor="@color/widget_color_on_background"
+            android:textSize="12sp"
+            tools:text="Jul 27" />
 
         <TextView
             android:id="@+id/message_text"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_below="@id/sender_text"
-            android:layout_marginTop="-2dp"
             android:layout_alignParentLeft="true"
+            android:layout_marginTop="-2dp"
             android:layout_toLeftOf="@+id/message_count"
-            android:textColor="@color/md_theme_light_onBackground"
-            android:textSize="12sp"
+            android:ellipsize="end"
             android:maxLines="2"
-            android:text="" />
+            android:text=""
+            android:textColor="@color/widget_color_on_background"
+            android:textSize="12sp"
+            tools:text="Message body" />
 
         <TextView
             android:id="@+id/message_count"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_alignParentRight="true"
             android:layout_alignBaseline="@id/message_text"
-            android:textColor="@color/md_theme_light_onBackground"
+            android:layout_alignParentRight="true"
+            android:maxLines="1"
+            android:text=""
+            android:textColor="@color/widget_color_on_background"
             android:textSize="12sp"
             android:textStyle="bold"
-            android:maxLines="1"
-            android:text="" />
+            tools:text="2" />
 
     </RelativeLayout>
 

+ 26 - 25
app/src/main/res/layout/widget_messages.xml

@@ -4,40 +4,42 @@
     android:id="@+id/widget_title"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:background="#eaeaea"
+    android:background="@color/widget_color_surface_container_low"
     android:orientation="vertical">
 
-    <!-- absolutely do not use attr references for colors in this file! -->
-    <RelativeLayout
+    <!-- Do not use ?attr/ references for colors in this layout file. A remote view can not resolve them -->
+
+    <LinearLayout
         android:layout_width="match_parent"
-        android:layout_height="48dp"
-        android:background="@color/md_theme_light_primary"
-        android:paddingLeft="16dp"
+        android:layout_height="wrap_content"
+        android:background="@color/widget_color_primary"
+        android:gravity="center_vertical"
+        android:paddingLeft="@dimen/grid_unit_x2"
         android:paddingRight="0dp">
 
-        <!-- do not use app:tint here! -->
+        <TextView
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginEnd="@dimen/grid_unit_x1"
+            android:layout_weight="1"
+            android:ellipsize="end"
+            android:maxLines="1"
+            android:text="@string/app_name"
+            android:textColor="@color/widget_color_on_primary"
+            android:textSize="20sp" />
+
+        <!-- Do not use app:tint here -->
         <ImageView
             android:id="@+id/widget_edit"
             android:layout_width="56dp"
             android:layout_height="48dp"
-            android:layout_alignParentRight="true"
-            android:layout_centerVertical="true"
+            android:contentDescription="@string/title_compose_message"
             android:scaleType="centerInside"
-            android:src="@drawable/ic_chat_bubble_bitmap_24dp"
-            android:tint="@color/md_theme_light_onPrimary"
+            android:src="@drawable/ic_chat_bubble"
+            android:tint="@color/widget_color_on_primary"
             tools:ignore="UseAppTint" />
 
-        <TextView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_alignParentLeft="true"
-            android:layout_centerVertical="true"
-            android:layout_toEndOf="@+id/widget_edit"
-            android:text="@string/app_name"
-            android:textColor="@color/md_theme_light_onPrimary"
-            android:textSize="20sp" />
-
-    </RelativeLayout>
+    </LinearLayout>
 
     <FrameLayout
         android:layout_width="match_parent"
@@ -54,10 +56,9 @@
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:gravity="center"
-            android:paddingLeft="16dp"
-            android:paddingRight="16dp"
+            android:paddingHorizontal="@dimen/grid_unit_x2"
             android:text="@string/no_unread_messages"
-            android:textColor="@color/md_theme_light_onSurface" />
+            android:textColor="@color/widget_color_on_surface" />
 
     </FrameLayout>
 

+ 3 - 1
app/src/main/res/values-be-rBY/strings.xml

@@ -231,6 +231,7 @@
     <string name="prefs_header_other">Іншае</string>
     <string name="an_error_occurred">Адбылася памылка</string>
     <string name="an_error_occurred_more">Адбылася памылка: \"%1$s\"</string>
+    <!-- Error message shown to the user if the app fails to start. ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
     <string name="acknowledge">Згодзен</string>
     <string name="decline">Не згодны</string>
     <string name="forward_message">Пераслаць паведамленне</string>
@@ -1114,7 +1115,8 @@
     <string name="continue_recording">Працягнуць запіс</string>
     <!-- ${app_name} is a placeholder for the app's name, e.g. "Threema". %1$s is a placeholder for the version number. They must not be translated. -->
     <string name="whatsnew_title">Што новага ў ${app_name} %1$s?</string>
-    <string name="whatsnew_headline"><![CDATA[<p>Цяпер вы можаце прывязаць сваю Android-прыладу да новай праграмы для ПК (бэта) і пратэставаць функцыю працы на некалькіх прыладах.</p>
+    <string name="whatsnew_headline"><![CDATA[
+<p>Цяпер вы можаце прывязаць сваю Android-прыладу да новай праграмы для ПК (бэта) і пратэставаць функцыю працы на некалькіх прыладах.</p>
         <p>Каб даведацца больш, перайдзіце ў «Меню > Threema 2.0 для ПК (Бэта)».</p>        ]]></string>
     <string name="tap_to_start">Націсніце тут, каб запусціць %s.</string>
     <string name="notification_title_restart">Патрабуецца перазапуск</string>

+ 94 - 4
app/src/main/res/values-bg/strings.xml

@@ -22,6 +22,7 @@
     <!-- ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
     <string name="enter_id_hint">Въведете ${app_name_short} идентификатор</string>
     <string name="account_links">Свързани акаунти</string>
+    <string name="title_set_availability_status">Достъпност</string>
     <string name="menu_settings">Настройки</string>
     <!-- ${app_name} is a placeholder for the app's name, it must be kept as-is, not translated. -->
     <string name="menu_about">За ${app_name}</string>
@@ -31,8 +32,12 @@
     <string name="title_job_title">Длъжност</string>
     <string name="title_department">Отдел</string>
     <string name="send">Изпратете</string>
+    <string name="prefs_linked_devices">Свързани устройства</string>
+    <string name="prefs_community_and_sharing">Общност &amp; споделяне</string>
     <!-- Title of section on "Settings" screen, for advanced settings -->
+    <string name="prefs_advanced">Разширени</string>
     <!-- Title of section on "Settings" screen, for information and other options about the app -->
+    <string name="prefs_about">Относно</string>
     <string name="prefs_privacy">Поверителност</string>
     <string name="prefs_notifications">Звук &amp; известия</string>
     <string name="prefs_chatdisplay">Чат</string>
@@ -85,7 +90,9 @@
     <string name="prefs_logging">Вписване</string>
     <string name="prefs_title_message_log_switch">Запишете лога във файл</string>
     <!-- Summary text shown under the "Log to file" switch in the settings. %1$s is a placeholder for the time since when the logging is enabled, %2$s is a placeholder for the total file size of the logs (e.g. 2.1MB) -->
+    <string name="prefs_title_sum_message_log_on_since">Логване на системната активност от %1$s (%2$s)</string>
     <!-- Summary text shown under the "Log to file" switch in the settings. %1$s is a placeholder for the name of a file which is used to force-enable the logging., %2$s is a placeholder for the total file size of the logs (e.g. 2.1MB) -->
+    <string name="prefs_title_sum_message_log_forced_on">Системната активност се логва. Изтрийте файла \"%1$s\", за да прекратите логването. (%2$s)</string>
     <string name="prefs_title_sum_message_log_off">Събитията няма да бъдат запазени в лога</string>
     <string name="prefs_reset_push">Рестартирайте push-токен</string>
     <string name="prefs_sum_reset_push">Пререгистрирайте устройството за push-известия с FCM/HMS</string>
@@ -115,6 +122,7 @@
     <string name="set_nickname_title">Задайте псевдоним</string>
     <string name="ok">Добре</string>
     <string name="cancel">Отменете</string>
+    <string name="save">Запазете</string>
     <string name="copy_message_action">Копирайте</string>
     <string name="delete_contact_action">Изтрийте контакт</string>
     <string name="delete_multiple_contact_action">Изтрийте контакти</string>
@@ -125,8 +133,10 @@
     <string name="scan_duplicate">Този идентификатор вече е сканиран и потвърден.</string>
     <string name="linked_email">Имейл</string>
     <!-- Pattern for displaying the user's email address on the My Profile screen. %1$s is a placeholder for the email address, %2$s is a placeholder for the verification state (e.g. "pending") -->
+    <string name="linked_email_pattern">%1$s (%2$s)</string>
     <string name="linked_mobile">Мобилен телефон</string>
     <!-- Pattern for displaying the user's mobile phone number on the My Profile screen. %1$s is a placeholder for the phone number, %2$s is a placeholder for the verification state (e.g. "pending") -->
+    <string name="linked_mobile_pattern">%1$s (%2$s)</string>
     <string name="public_nickname">Псевдоним</string>
     <string name="share_via">Изпрати чрез…</string>
     <string name="share_subject">Чат в %s</string>
@@ -191,7 +201,8 @@
     <string name="click_here_to_change_passphrase">Натиснете тук, за да промените тайната фраза</string>
     <string name="attach_camera">Камера</string>
     <string name="menu_restore">Възстановете от резервно копие</string>
-    <string name="restore_id_hint">Въведете или поставете текстовата секвенция на експортирания идентификатор, за да възстановите</string>
+    <string name="restore_id_hint">Въведете текста от експорта на идентификатора, за да възстановите. Може и да сканирате QR кода от Вашия експортиран идентификатор.</string>
+    <string name="restore_id_string_label">Откъс от текст за експортиране на идентификатора</string>
     <string name="location_placeholder">Местоположение</string>
     <string name="video_placeholder">Видео</string>
     <string name="audio_placeholder">Аудио</string>
@@ -227,6 +238,8 @@
     <string name="prefs_header_other">Друго</string>
     <string name="an_error_occurred">Появи се грешка</string>
     <string name="an_error_occurred_more">Появи се грешка: „%1$s“</string>
+    <!-- Error message shown to the user if the app fails to start. ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
+    <string name="startup_error_details">${app_name_short} не може да се отвори поради неочаквана грешка. Моля, експортирайте журнала/лога по-долу и се свържете с нашия екип по поддръжка, за да разгледаме случая. Извиняваме се за неудобството.</string>
     <string name="acknowledge">Съгласие</string>
     <string name="decline">Несъгласие</string>
     <string name="forward_message">Препратете съобщението</string>
@@ -344,11 +357,15 @@
     <string name="status_rename_group">Групата е преименувана на „%1$s“</string>
     <string name="status_group_new_photo">Снимката на групата е обновена.</string>
     <string name="status_group_new_member">%1$s бе добавен в групата.</string>
+    <string name="status_group_you_were_added">Бяхте добавен към групата.</string>
     <string name="status_group_member_left">%1$s напусна групата.</string>
+    <string name="status_group_you_left">Вие напуснахте групата.</string>
     <string name="status_group_member_kicked">%1$s бе отстранен от групата.</string>
+    <string name="status_group_you_were_removed">Бяхте премахнат от групата.</string>
     <string name="can_not_send_no_group_members">Не можете да изпращате съобщения в празна група</string>
     <string name="you_are_not_a_member_of_this_group">Вие не сте член на тази група</string>
     <string name="title_public_nickname">Псевдоним</string>
+    <string name="title_availability_status">Статус</string>
     <string name="prefs_sum_excluded_sync_identities">Идентификаторите, които са изброени тук, ще бъдат игнорирани при синхронизация на контактите</string>
     <string name="prefs_title_excluded_sync_identities">Списък с изключения</string>
     <string name="synchronize_contact">Синхронизация на контактите от телефона</string>
@@ -477,6 +494,7 @@
     <string name="ballot_result_final">Краен резултат</string>
     <string name="invalid_cannot_send">Не можете да изпращате съобщения на невалиден контакт</string>
     <string name="ballot_answer_count_error">Моля, въведете поне два варианта за гласуване в анкетата.</string>
+    <string name="ballot_removed">Анкетата е премахната</string>
     <string name="contact_state_inactive">Неактивен</string>
     <string name="contact_state_invalid">Невалиден</string>
     <string name="back">Назад</string>
@@ -484,6 +502,7 @@
     <string name="wearable_reply_label">Отговорете на %s</string>
     <!-- ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
     <string name="push_disable_text">Ако продължите, „${app_name_short} Push“ ще бъде използван вместо системните push-известия. Това помага да се намали Вашият дигитален отпечатък, но изисква телефонът Ви да бъде настроен, така че приложението да се изпълнява на заден план. Може да се наложи да се свържете с производителя на устройството за помощ с настройката, ако тази опция отсъства в настройките на устройството.</string>
+    <string name="debug_log_enabled_recently_warning">Ако имате специфичен проблем, моля да се опитате да го възпроизведете, а след това ни изпратете дебъг/лог файла. Това ще ни помогне да проучим проблема по-ефективно. Благодарим Ви.</string>
     <string name="ballot_intermediate_results_show">Покажете временните резултати</string>
     <string name="converting_video">Обработка на видеото</string>
     <string name="video_size_small">Висока (по-малко данни)</string>
@@ -596,7 +615,7 @@
     <string name="no_app_for_location">Няма инсталирано приложение с карти на това устройство</string>
     <string name="prefs_sendlog">Изпратете лог</string>
     <!-- ${company_name} is a placeholder for the name of the company that operates the app, e.g. "Threema". It must be left as-is and not translated. -->
-    <string name="prefs_sendlog_summary">Изпратете лог на ${company_name} за последващ анализ, ако имате проблеми</string>
+    <string name="prefs_sendlog_summary">Изпратете лог/журнал на ${company_name} за последващ анализ, ако имате проблеми</string>
     <string name="prefs_exportlog">Експортирайте лог</string>
     <string name="prefs_exportlog_summary">Изпратете лог-файл с друго приложение</string>
     <!-- ${app_name} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
@@ -675,7 +694,7 @@
          допълнителен метод да Ви идентифицират потребители, които още не са Ви добавили в техните списъци с контакти. Ние препоръчваме
        да предоставите само името Ви или псевдоним. Ако не добавите псевдоним, ние ще използваме Вашия ${app_name_short} идентификатор по подразбиране.</string>
     <string name="not_linked">не е свързан</string>
-    <string name="linked">свързан</string>
+    <string name="linked">Свързан</string>
     <string name="pending_sms_verification_notice">Вашият телефонен номер все още не е потвърден.</string>
     <string name="no_sms_received">Не получихте ли СМС?</string>
     <string name="really_cancel_verify">Наистина ли искате да прекратите процеса по верификация на телефонния номер?</string>
@@ -688,7 +707,7 @@
     <!-- Shown in prompt dialog during the setup wizard. Only used for work builds (i.e. Threema Work and OnPrem). ${app_name} and ${app_name_short} are placeholders for the app's name, they must be kept as-is and not translated. -->
     <string name="new_wizard_anonymous_confirm">Не сте въвели мобилен телефон или имейл, които да се свържат с Вашия ${app_name_short} идентификатор. Вие няма да се виждате в списъка с контакти на Вашите приятели. Наистина ли искате да използвате ${app_name} анонимно?</string>
     <!-- ${app_name} and ${app_name_short} are placeholders for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
-    <string name="new_wizard_anonymous_confirm_phone_only">Не сте въвели мобилен телефон, който да се свърже с Вашия ${app_name_short} идентификатор. Вие няма да се виждате в списъка с контакти на Вашите приятели. Наистина ли искате да използвате ${app_name} анонимно?</string>
+    <string name="new_wizard_anonymous_confirm_phone_only">Не сте въвели мобилен телефон, който да се свърже с Вашия ${app_name_short} идентификатор. Следователно няма да бъдете добавен автоматично в списъците с контакти на Вашите приятели или колеги. Наистина ли искате да използвате ${app_name} анонимно?</string>
     <string name="error_saving_file">Някои файлове не могат да бъдат запаметени</string>
     <string name="wait_one_minute">Моля, изчакайте поне 10 минути за СМС-а преди да поискате телефонно обаждане.</string>
     <string name="backup_id">Експортиран идентификатор</string>
@@ -772,6 +791,7 @@
     <!-- ${app_name_short} are placeholders for the app's name, e.g. "Threema". They must be left as-is and not translated. -->
     <string name="threema_channel_intro">${app_name_short} каналът Ви предоставя новини за всичко около ${app_name_short}. Искате ли да се абонирате за канала на ${app_name_short} и да го добавите като контакт? Безплатно е и винаги можете да се отпишете.</string>
     <!-- Name of action in context menu for a chat message, which lets the user write a reply to that message -->
+    <string name="reply_to_message">Отговорете</string>
     <string name="contacts_deleted">Контактът е изтрит</string>
     <string name="take_photo">Направете снимка</string>
     <string name="select_from_gallery">Изберете от галерията</string>
@@ -816,6 +836,7 @@
     <string name="play">Възпроизведете</string>
     <string name="pause">Паузирайте</string>
     <string name="retry">Повторете</string>
+    <string name="voice_message">Гласово съобщение</string>
     <string name="voice_message_record">Запишете гласово съобщение</string>
     <string name="open_navdrawer">Отворете навигационния панел</string>
     <string name="profile_picture">Профилна снимка</string>
@@ -925,6 +946,7 @@
     <string name="password_too_short_generic">Паролата е твърде кратка</string>
     <string name="passwords_dont_match">Паролите не съвпадат</string>
     <!-- Error message shown when setting up a custom server for the Threema Safe feature. %s is a placeholder for the details of the error. -->
+    <string name="threema_safe_test_unsuccessful">Неуспешен тест: %s</string>
     <!-- ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
     <string name="preparing_threema_safe">Подготовка на ${app_name_short} Safe</string>
     <string name="disable_powermanager_title">Енергийни ограничения</string>
@@ -1106,6 +1128,7 @@
         <p>За повече информация, отидете в „Меню - Threema 2,0 за настолен компютър (бета)“.</p>
         ]]></string>
     <string name="tap_to_start">Натиснете тук, за да започнете %s сега.</string>
+    <string name="notification_title_restart">Изисква се рестартиране</string>
     <string name="two_years">2 години</string>
     <string name="backup_data_no_permission">Не може да се записва в тази директория. Моля, изберете друга.</string>
     <string name="prefs_sum_show_unread_badge">Показване на индикатор за броя непрочетени съобщения</string>
@@ -1211,7 +1234,9 @@
     <string name="error_unable_loading_media_thumb">Грешка. Миниатюрата не може да бъде заредена</string>
     <string name="filter_list">Филтрирайте списъка</string>
     <!-- A hint/placeholder text that is shown when a search field appears. The user needs to enter a search query -->
+    <string name="search_list">Търсене в списъка</string>
     <string name="hint_filter_list">Въведете текст на филтъра</string>
+    <string name="hint_search_list">Въведете текст за търсене</string>
     <string name="add">Добавете</string>
     <string name="only_images_or_videos">Само изображения или видеа могат да се избират</string>
     <string name="media_gallery_gifs">GIF файлове</string>
@@ -1526,10 +1551,13 @@
     <string name="problemsolver_title_background_data">Данните на заден план са ограничени</string>
     <string name="problemsolver_title_notifications">Известията са деактивирани</string>
     <string name="problemsolver_title_fullscreen_notifications">Не са разрешени известията при цял екран</string>
+    <string name="problemsolver_title_debug_log_enabled">Логването още е активно</string>
     <string name="problemsolver_explain_fullscreen_notifications">На приложението не му е разрешено да показва известия на цял екран. Може да пропуснете входящите обаждания, когато устройството е заключено. За да го оправите, активирайте „Позволете известията на цял екран.“</string>
     <string name="problemsolver_explain_notifications">Известията са напълно деактивирани за това приложение. Няма да бъдете информирани за входящите съобщения или обаждания и приложението може и да не работи правилно. За да го поправите, активирайте „Всички известия.“</string>
     <string name="problemsolver_explain_background_data">Приложението не може да използва мобилни данни, когато е във фонов режим. Няма да бъдете информирани за входящи съобщения, освен ако не отворите самото приложение. За да поправите това, активирайте „Данни във фонов режим.“</string>
     <string name="problemsolver_explain_background">Ограничена е възможността за работа във фонов режим. Няма да получавате известия и много функции няма да работят правилно. За да поправите това, натиснете на „Използване на батерията от приложението“ и изберете „Неограничено.“</string>
+    <string name="problemsolver_explain_debug_log_enabled">Преди известно време активирахте дебъг лога. Ако вече не желаете да логвате събития и данни за мрежата, можете да деактивирате логването отново.</string>
+    <string name="problemsolver_explain_debug_log_force_enabled">${app_name} понастоящем логва събития и данни за мрежата във файл, защото създадохте файла \"%s\". Ако не се нуждаете повече от водене на журнал, можете да изтриете файла.</string>
     <string name="problemsolver_info_text">Наименованията на някои от тези системни настройки, които се споменават по-горе, може да се различават в зависимост от версията на Android и от производителя на устройството.</string>
     <string name="problemsolver_title_app_battery_usgae_optimized">Използването на батерията е настроено на “оптимизирано“</string>
     <!-- ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
@@ -1556,11 +1584,27 @@
     <string name="incoming_calls">Входящи повиквания</string>
     <string name="group_chats">Групови чатове</string>
     <string name="edit_history_file_no_caption">История на файл без заглавие</string>
+    <string name="prefs_title_error_reporting">Сигнал за грешка</string>
+    <string name="prefs_send_error_reports">Изпратете анонимен доклад</string>
+    <string name="error_reporting_option_never_send">Никога</string>
+    <string name="error_reporting_option_always_send">Винаги</string>
+    <string name="error_reporting_option_always_ask">Попитай ме всеки път</string>
     <!-- ${company_name} is a placeholder for the name of the company that operates the app, e.g. "Threema". It must be left as-is and not translated. -->
+    <string name="error_reporting_summary_never_send">Никога не изпращайте отчети за грешки на ${company_name}</string>
     <!-- ${company_name} is a placeholder for the name of the company that operates the app, e.g. "Threema". It must be left as-is and not translated. -->
+    <string name="error_reporting_summary_always_send">Автоматично изпращане на отчети за грешки до ${company_name}, когато се засече критична грешка</string>
     <!-- ${company_name} is a placeholder for the name of the company that operates the app, e.g. "Threema". It must be left as-is and not translated. -->
+    <string name="error_reporting_summary_always_ask">Попитай ме всеки път дали да се изпрати доклад за грешки на ${company_name}</string>
+    <string name="error_detected_dialog_title">Изпратете анонимен доклад</string>
+    <string name="crash_detected_dialog_text">Изглежда, че наскоро е възникнал проблем. Желаете ли да изпратите анонимен отчет за грешката?</string>
+    <string name="crash_detected_dialog_remember_choice">Запомни избора ми</string>
+    <string name="crash_detected_dialog_send_button">Изпратете</string>
+    <string name="error_reporting_learn_more_button">Научете повече</string>
+    <string name="error_reporting_details_button">Технически подробности</string>
     <!-- Shown in the advanced settings, shows the user their own Device's uniquely generated ID, which is used for sending error reports -->
+    <string name="prefs_error_reporting_id">Анонимен идентификатор на устройството</string>
     <!-- The %s is a placeholder for an ID. It must be left as-is. -->
+    <string name="prefs_error_reporting_id_summary">Този идентификатор се използва за обединяване на доклади за грешки. Той е анонимен и не е свързан с Вашия Threema идентификатор.\n%s</string>
     <string name="md_device_currently_active">Активни в момента</string>
     <string name="md_device_last_active_at">Последно активен: %s</string>
     <plurals name="md_max_device_slots_reached_info">
@@ -1579,9 +1623,13 @@
     <string name="md_drop_device_failed_dialog_message">Моля, проверете интернет връзката Ви и опитайте отново.</string>
     <string name="md_drop_device_failed_dialog_button_close">Затворете</string>
     <!-- Message shown in dialog when the user starts configuring a backup, but we detected that there might be a problem and thus offer a workaround option to choose from -->
+    <string name="backup_path_selection_message">Открит е потенциален проблем при избора на папка. Ако проблемът продължи, може да запазите резервното копие в папката „Документи“, а не в избрана от Вас папка.</string>
     <!-- Button label on dialog when configuring a backup, shown on dialog that lets the user choose how they want to select the backup folder. This option lets them use the regular file picker. -->
+    <string name="backup_path_selection_default">Изберете папка</string>
     <!-- Button label on dialog when configuring a backup, shown on dialog that lets the user choose how they want to select the backup folder. This option lets them bypass the file picker and use the default Documents folder instead -->
+    <string name="backup_path_selection_documents">Използвайте папката “Документи”</string>
     <!-- accessibility -->
+    <string name="cd_menu_more">Още опции</string>
     <string name="cd_edited">съобщението е редактирано</string>
     <string name="cd_created_at">създадено: %s</string>
     <string name="cd_edited_at">редактирано: %s</string>
@@ -1640,6 +1688,8 @@
     <string name="device_linking_error_invalid_contact_title">Невалиден контакт</string>
     <!-- Error message when linking of a new device failed due to an invalid contact. %1$s will be replaced with the threema identity of the affected contact. -->
     <string name="device_linking_error_invalid_contact_body">Контактът %1$s е запазен неправилно на това устройство и пречи на свързването с новото устройство. Моля, свържете се с поддръжката (\"Основно меню &gt; Помощ\").</string>
+    <string name="device_linking_error_invalid_timestamp_title">Невалидна дата</string>
+    <string name="device_linking_error_invalid_timestamp_body">Открита е невалидна дата. Моля, свържете се с поддръжката („Главно меню &gt; Помощ“).</string>
     <string name="work_directory_title">Директория на фирмата</string>
     <!-- Instruction text for the user directory search -->
     <string name="work_directory_search">Потърсете във фирмената директория</string>
@@ -1647,12 +1697,15 @@
     <string name="work_directory_empty_view_text">Моля, въведете поне 3 букви от името, за да започне търсенето във фирмената директория с ${app_name_short} потребители.</string>
     <!-- Message that is shown when the user wants to share an invalid file. -->
     <string name="invalid_file_cannot_be_shared">Този файл е невалиден и не може да бъде споделен</string>
+    <string name="availability_status_none">Няма статус</string>
     <!-- Status (only relevant for Work), e.g. as shown on their profile, indicating that the person is currently not available, i.e. out-of-office -->
     <string name="availability_status_unavailable">Недостъпен</string>
     <!-- Status (only relevant for Work), e.g. as shown on their profile, indicating that the person is currently busy-->
+    <string name="availability_status_busy">Зает</string>
     <!-- Error message (only relevant for OnPrem) if the preset server url is different from the one provided by the activation link. -->
     <string name="error_preset_onprem_url_mismatch_intent">Предоставеният линк за активация е невалиден. Моля, свържете се с Вашия администратор.</string>
     <!-- Warning message displayed on screen where the user can export a debug log file to share with Support. ${company_name} is a placeholder for the company name and should be kept as-is, not translated. -->
+    <string name="export_logs_screen_warning">За запазване на поверителността, споделяйте лог файла само с екипа по поддръжка на ${company_name} и само ако сте помолени да го направите.</string>
     <string name="work_intro_screen_work_title">ИЗКЛЮЧИТЕЛНО СИГУРНА КОМУНИКАЦИЯ ЗА ФИРМИТЕ</string>
     <string name="work_intro_screen_work_subtitle">Threema Work е приложение за сигурна комуникация между фирми и компании, което е криптирано от край до край, съобразено с GDPR и е разработено в Швейцария.</string>
     <string name="work_intro_screen_work_sign_in_button_label">Впишете се сега</string>
@@ -1668,10 +1721,13 @@
     <string name="work_intro_screen_onprem_use_private_hint">Ако искате да използвате Threema за лични нужди, то ние Ви препоръчваме да използвате частното Threema приложение.</string>
     <string name="work_intro_screen_onprem_download_private_button_label">Изтеглете сега</string>
     <!-- Description of the notification channel which is used when DualLock (a.k.a. "Remote Secret") is active. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
+    <string name="remote_secret_notification_channel_description">Показва за постоянно известие, когато ${remote_secret} е активно.</string>
     <!-- Text of the notification which is shown when DualLock (a.k.a. "Remote Secret") is active. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
+    <string name="remote_secret_notification_text">${remote_secret} е активно. Натиснете, за да научите повече.</string>
     <!-- Message shown fullscreen when the app is opened, to inform the user that their access has been blocked by an admin. Only used for OnPrem builds. -->
     <string name="remote_secrets_blocked_by_admin">Достъпът Ви до това приложение е блокиран от Вашия администратор.\n\nМоля, свържете се с администратора за повече информация.</string>
     <!-- Label on button, shown when the app is blocked with the DualLock (a.k.a. "Remote Secret") feature and the user has no other option than to close the app -->
+    <string name="remote_secrets_close_app">Затворете приложението</string>
     <!-- Message shown fullscreen when the app is opened, to inform the user that the DualLock (a.k.a. "Remote Secret") needed to unlock the app could not be fetched due to a network issue. Shown above a "Retry" button. Only used for OnPrem builds. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
     <string name="remote_secrets_failed_to_fetch">${remote_secret} токенът не може да бъде извлечен. Моля, проверете интернет връзката Ви и опитайте отново.</string>
     <!-- Shown on loading spinner after app is started while DualLock (a.k.a. "Remote Secret") is fetched from the server to unlock the app. Only used for OnPrem builds. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
@@ -1692,8 +1748,41 @@
     <string name="remote_secret_deactivating">Деактивиране ${remote_secret}</string>
     <!-- Error message shown fullscreen when deactivating the DualLock (a.k.a. "Remote Secret") feature failed. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
     <string name="remote_secret_deactivating_failed">Деактивацията на ${remote_secret} е неуспешна. Моля, проверете интернет връзката Ви и опитайте отново.</string>
+    <string name="referral_banner_title">Препоръчайте и спечелете</string>
+    <string name="referral_banner_body">Поканете Вашите приятели да използват Threema Work в тяхната фирма и спечелете бонус за препоръка!</string>
+    <string name="referral_screen_title">Препоръчайте и спечелете</string>
+    <string name="referral_screen_subtitle">Получете до 10% от първата фактура на препоръчаната компания.</string>
+    <string name="referral_how_it_works_title">Как работи</string>
+    <string name="referral_how_it_works_referral_title">Препоръка</string>
+    <string name="referral_how_it_works_referral_body">Поканете приятел или неговата компания да се присъедини към Threema Work. Важно е Вашият Threema идентификатор да бъде посочен при регистрацията или при закупуването на лицензи, за да бъде отчетена препоръката Ви.</string>
+    <string name="referral_how_it_works_requirements_title">Изисквания</string>
+    <string name="referral_how_it_works_requirements_body">Препоръчаната фирма не бива да е вече наш клиент и трябва да поръча лицензи</string>
+    <string name="referral_how_it_works_confirmation_title">Потвърждение</string>
+    <string name="referral_how_it_works_confirmation_body">За да гарантираме, че Вашата препоръка е правилно асоциирана и потвърдена, ние ще се свържем с Вас преди да направим плащането. Ще Ви информираме веднага щом проверката приключи.</string>
+    <string name="referral_your_reward_title">Вашата награда</string>
+    <string name="referral_your_reward_subtitle">Спечелете бонус за препоръка</string>
+    <string name="referral_your_reward_body">За Вашата препоръка, Вие ще получите 10% от първата фактура на препоръчаната фирма (без данъци/такси). Бонусът ще Ви бъде прехвърлен до 30-60 дни след плащането на фактурата.</string>
+    <string name="referral_share_invitation_link">Споделете линк за покана</string>
+    <string name="referral_view_terms_and_conditions">Вижте Условия &amp; ползване</string>
+    <string name="referral_invitation_message_content">Здравейте, мисля, че това може да Ви бъде полезно: Threema Work е идеална опция за сигурна бизнес комуникация. Това е Вашият тестови линк: %1$s</string>
+    <string name="prefs_voip_abort_on_bluetooth_disconnect_title">Прекратяване на разговора, когато Блутуутът е изключен</string>
+    <string name="prefs_voip_abort_on_bluetooth_disconnect_summary_on">Обаждането приключва, когато Блутуут слушалките се изключат по време на разговор.</string>
+    <string name="prefs_voip_abort_on_bluetooth_disconnect_summary_off">Обаждането ще продължи, ако Блутуут слушалките се изключат по време на обаждане. Внимание: Това може да деактивира бутона за затваряне на разговора на слушалките.</string>
+    <string name="failed_to_load_conversations">Невъзможно е да се заредят чатовете.</string>
+    <string name="contact_support">Свържете се с поддръжката</string>
+    <string name="edit_availability_status_modal_title">Достъпност</string>
+    <string name="edit_availability_status_modal_subtitle">Този статус е видим за всички във фирмата.</string>
+    <string name="edit_availability_status_modal_optional_message_label">Персонализиран текст (По избор)</string>
+    <string name="edit_availability_status_modal_optional_message_placeholder">В среща</string>
+    <string name="edit_availability_status_modal_optional_message_description">Задайте персонализирано съобщение за състояние, когато сте избрали Зает или Недостъпен.</string>
+    <string name="edit_availability_status_modal_optional_message_too_long">Текстът е много дълъг и ще трябва да го съкратите</string>
+    <string name="tooltip_title_availability_status">Достъпност</string>
+    <string name="tooltip_text_availability_status_in_chat">Вижте дали някой е зает или недостъпен преди да му пишете. Може да зададете собствения Ви статус в профила.</string>
+    <string name="tooltip_text_availability_status_in_profile">Задайте Вашия статус, за да покажете кога сте зает или недостъпен.</string>
+    <string name="view_full_availability_status_modal_title">Достъпност</string>
     <!-- A hint text that will be played by the android screen reader in accessibility mode. It explains the effect of a click action. -->
     <!-- Clicking will open a new modal view that displays the full length of a contact's availability status description. -->
+    <string name="accessibility_view_full_availability_status">Докоснете, за да видите пълния статус на достъпност</string>
     <plurals name="contacts_counter_label">
         <item quantity="one">%d контакт</item>
         <item quantity="other">%d контакти</item>
@@ -1747,6 +1836,7 @@
         <item quantity="other">%d изтрити съобщения</item>
     </plurals>
     <!-- Used for indicating for how long the notifications of a chat are muted -->
+    <string name="notifications_for_one_week">За 1 седмица</string>
     <plurals name="notifications_for_x_hours">
         <item quantity="one">За %d час</item>
         <item quantity="other">За %d часа</item>

+ 6 - 0
app/src/main/res/values-bg/voip_strings.xml

@@ -53,12 +53,18 @@
     <string name="voip_prefs_video_codec_no_h264hip">Деактивирайте H264-HiP</string>
     <string name="voip_prefs_video_codec_sw">Софтуерни кодеци</string>
     <string name="voip_connection_failed">Връзката не може да бъде осъществена</string>
+    <string name="voip_call_disconnected">Обаждането прекъсна</string>
     <string name="prefs_voip_reject_incoming_calls_title">Отклонете мобилните обаждания</string>
     <string name="prefs_voip_reject_incoming_calls_summary">Отклонете мобилните обаждания докато има активно обаждане по Threema.</string>
     <string name="voip_contact_not_found">Няма Threema контакт за този номер.</string>
     <string name="voip_another_pstn_call">Не може да се осъществи обаждане. Има активно мобилно обаждане.</string>
     <string name="voip_call_status_off_hours">Обаждане в неработно време</string>
     <string name="voip_peer_video_disabled">Събеседникът деактивира видео разговора.</string>
+    <string name="voip_error_processing_call_anser">Грешка при обработването на обаждането</string>
+    <string name="voip_error_peer_connection">Грешка в Peer връзката</string>
+    <string name="voip_error_determine_common_video_quality_profile">Не може да се определи общ профил на видео качеството</string>
+    <string name="voip_audio_focus_lost">Звуковият фокус е изгубен</string>
+    <string name="voip_could_not_initialize">Обаждането не може да се стартира</string>
     <!-- WebRTC debugger -->
     <string name="voip_prefs_webrtc_debug">WebRTC Диагностика</string>
     <string name="voip_prefs_webrtc_debug_summary">Стартирайте този инструмент, за да дебъгнете проблеми със започването на обаждане</string>

+ 1 - 0
app/src/main/res/values-ca/strings.xml

@@ -218,6 +218,7 @@ de continuar.</string>
     <string name="prefs_header_other">Altres</string>
     <string name="an_error_occurred">Hi ha hagut un error</string>
     <string name="an_error_occurred_more">Hi ha hagut un error: «%1$s»</string>
+    <!-- Error message shown to the user if the app fails to start. ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
     <string name="acknowledge">D\'acord</string>
     <string name="decline">En desacord</string>
     <string name="forward_message">Reenviar el missatge</string>

+ 2 - 0
app/src/main/res/values-cs/strings.xml

@@ -238,6 +238,8 @@
     <string name="prefs_header_other">Ostatní</string>
     <string name="an_error_occurred">Došlo k chybě</string>
     <string name="an_error_occurred_more">Došlo k chybě: „%1$s“</string>
+    <!-- Error message shown to the user if the app fails to start. ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
+    <string name="startup_error_details">Aplikaci ${app_name_short} se nezdařilo otevřít z důvodu neočekávané chyby. Exportujte prosím níže uvedený záznam a kontaktujte naši podporu, abychom mohli vzniklé potíže prošetřit. Za vzniklé nepříjemnosti se omlouváme.</string>
     <string name="acknowledge">Souhlasím</string>
     <string name="decline">Nesouhlasím</string>
     <string name="forward_message">Přeposlat zprávu</string>

+ 2 - 0
app/src/main/res/values-de/strings.xml

@@ -249,6 +249,8 @@
     <string name="prefs_header_other">Weitere</string>
     <string name="an_error_occurred">Es ist ein Fehler aufgetreten</string>
     <string name="an_error_occurred_more">Es ist ein Fehler aufgetreten: «%1$s»</string>
+    <!-- Error message shown to the user if the app fails to start. ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
+    <string name="startup_error_details">${app_name_short} konnte aufgrund eines unerwarteten Fehlers nicht geöffnet werden. Bitte exportieren Sie die Logdatei und kontaktieren Sie unser Support-Team, damit wir das Problem untersuchen können. Wir entschuldigen uns für die Unannehmlichkeiten.</string>
     <string name="acknowledge">Bestätigen</string>
     <string name="decline">Ablehnen</string>
     <string name="forward_message">Weiterleiten</string>

+ 2 - 0
app/src/main/res/values-es/strings.xml

@@ -239,6 +239,8 @@
     <string name="prefs_header_other">Más</string>
     <string name="an_error_occurred">Se ha producido un error</string>
     <string name="an_error_occurred_more">Se ha producido un error: «%1$s»</string>
+    <!-- Error message shown to the user if the app fails to start. ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
+    <string name="startup_error_details">No se ha podido abrir ${app_name_short} por un error inesperado. Exporte el archivo de registro a continuación y contacte con nuestro equipo de asistencia para que podamos investigar el problema. Lamentamos las molestias ocasionadas.</string>
     <string name="acknowledge">Confirmar</string>
     <string name="decline">Rechazar</string>
     <string name="forward_message">Reenviar</string>

+ 2 - 0
app/src/main/res/values-fr/strings.xml

@@ -238,6 +238,8 @@
     <string name="prefs_header_other">Autre</string>
     <string name="an_error_occurred">Une erreur s\'est produite</string>
     <string name="an_error_occurred_more">Une erreur s\'est produite : « %1$s »</string>
+    <!-- Error message shown to the user if the app fails to start. ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
+    <string name="startup_error_details">Échec de l\'ouverture de ${app_name_short} en raison d\'une erreur inconnue. Veuillez exporter le fichier journal ci-dessous et contacter notre équipe d\'assistance pour que nous puissions enquêter sur le problème. Nous nous excusons pour la gêne occasionnée.</string>
     <string name="acknowledge">Accuser réception</string>
     <string name="decline">Désapprouver</string>
     <string name="forward_message">Transférer le message</string>

+ 4 - 2
app/src/main/res/values-gsw/strings.xml

@@ -238,6 +238,8 @@
     <string name="prefs_header_other">Wiiteri</string>
     <string name="an_error_occurred">Es isch en Fehler ufträtte</string>
     <string name="an_error_occurred_more">Es isch en Fehler ufträtte: «%1$s»</string>
+    <!-- Error message shown to the user if the app fails to start. ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
+    <string name="startup_error_details">${app_name_short} hät wägeme unerwartete Fehler nöd chöne göffnet werde. Bitte exportiered Sie d’Logdatei und kontaktiered Sie üses Support-Team, demit mer s’Problem chönd gnauer aluege. Mer entschuldiged üs für d’Unannehmlichkeite.</string>
     <string name="acknowledge">Bestätige</string>
     <string name="decline">Ablehne</string>
     <string name="forward_message">Wiiterleite</string>
@@ -613,8 +615,8 @@
     <string name="prefs_sendlog">Logdatei verschicke</string>
     <!-- ${company_name} is a placeholder for the name of the company that operates the app, e.g. "Threema". It must be left as-is and not translated. -->
     <string name="prefs_sendlog_summary">Logdatei für Problemanalyse a ${company_name} schicke</string>
-    <string name="prefs_exportlog">Logdatei teile</string>
-    <string name="prefs_exportlog_summary">Logdatei für Problemanalyse teile</string>
+    <string name="prefs_exportlog">Logdatei exportiere</string>
+    <string name="prefs_exportlog_summary">Logdatei über e anderi App teile</string>
     <!-- ${app_name} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
     <string name="permission_storage_required">Aktiviered Sie d’Speicher-Berächtigung für ${app_name}, zum Medie z’sichere oder z’schicke.</string>
     <!-- ${app_name} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->

+ 1 - 0
app/src/main/res/values-hu/strings.xml

@@ -222,6 +222,7 @@
     <string name="prefs_header_other">További</string>
     <string name="an_error_occurred">Hiba történt</string>
     <string name="an_error_occurred_more">Hiba történt: «%1$s»</string>
+    <!-- Error message shown to the user if the app fails to start. ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
     <string name="acknowledge">Megerősítés</string>
     <string name="decline">Visszautasítás</string>
     <string name="forward_message">Előre</string>

+ 2 - 0
app/src/main/res/values-it/strings.xml

@@ -252,6 +252,8 @@
     <string name="prefs_header_other">Altri</string>
     <string name="an_error_occurred">Si è verificato un errore</string>
     <string name="an_error_occurred_more">Si è verificato un errore: \"%1$s\"</string>
+    <!-- Error message shown to the user if the app fails to start. ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
+    <string name="startup_error_details">Impossibile aprire ${app_name_short} a causa di un errore imprevisto. Esporta il seguente file di log e contatta il nostro team di assistenza in modo che possiamo esaminare il problema. Ci scusiamo per l\'inconveniente.</string>
     <string name="acknowledge">Conferma</string>
     <string name="decline">Non approvo</string>
     <string name="forward_message">Inoltra</string>

+ 1 - 0
app/src/main/res/values-ja/strings.xml

@@ -226,6 +226,7 @@
     <string name="prefs_header_other">その他</string>
     <string name="an_error_occurred">エラーが発生しました</string>
     <string name="an_error_occurred_more">エラーが発生しました: «%1$s»</string>
+    <!-- Error message shown to the user if the app fails to start. ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
     <string name="acknowledge">賛成</string>
     <string name="decline">反対</string>
     <string name="forward_message">メッセージを転送する</string>

+ 10 - 0
app/src/main/res/values-night/colors.xml

@@ -46,4 +46,14 @@
     <color name="availability_status_icon_busy">#F6A421</color>
     <color name="availability_status_container_busy">#56480D</color>
     <color name="availability_status_on_container_busy">#FFFFFF</color>
+
+    <!-- Home Screen Widget -->
+    <!-- We need to define them here, as a remote view cannot resolve ?attr/ references -->
+    <color name="widget_color_primary">@color/md_theme_dark_primary</color>
+    <color name="widget_color_on_primary">@color/md_theme_dark_onPrimary</color>
+    <color name="widget_color_surface_container_low">@color/md_theme_dark_surfaceContainerLow</color>
+    <color name="widget_color_on_surface">@color/md_theme_dark_onSurface</color>
+    <color name="widget_color_background">@color/md_theme_dark_background</color>
+    <color name="widget_color_on_background">@color/md_theme_dark_onBackground</color>
+
 </resources>

+ 1 - 0
app/src/main/res/values-nl-rNL/strings.xml

@@ -229,6 +229,7 @@
     <string name="prefs_header_other">Overig</string>
     <string name="an_error_occurred">Er is een fout opgetreden</string>
     <string name="an_error_occurred_more">Er is een fout opgetreden: «%1$s»</string>
+    <!-- Error message shown to the user if the app fails to start. ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
     <string name="acknowledge">Akkoord</string>
     <string name="decline">Niet akkoord</string>
     <string name="forward_message">Bericht doorsturen</string>

+ 1 - 0
app/src/main/res/values-no/strings.xml

@@ -219,6 +219,7 @@ når den første meldingen mottas.</string>
     <string name="prefs_header_other">Annet</string>
     <string name="an_error_occurred">Det oppstod en feil</string>
     <string name="an_error_occurred_more">Det oppstod en feil: “%1$s”</string>
+    <!-- Error message shown to the user if the app fails to start. ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
     <string name="acknowledge">Enig</string>
     <string name="decline">Uenig</string>
     <string name="forward_message">Videresend melding</string>

+ 1 - 0
app/src/main/res/values-pl/strings.xml

@@ -229,6 +229,7 @@
     <string name="prefs_header_other">Inne</string>
     <string name="an_error_occurred">Wystąpił błąd</string>
     <string name="an_error_occurred_more">Wystąpił błąd: «%1$s»</string>
+    <!-- Error message shown to the user if the app fails to start. ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
     <string name="acknowledge">Potwierdź</string>
     <string name="decline">Nie zgadzam się</string>
     <string name="forward_message">Prześlij wiadomość</string>

+ 1 - 0
app/src/main/res/values-pt-rBR/strings.xml

@@ -229,6 +229,7 @@
     <string name="prefs_header_other">Outros</string>
     <string name="an_error_occurred">Ocorreu um erro</string>
     <string name="an_error_occurred_more">Ocorreu um erro: «%1$s»</string>
+    <!-- Error message shown to the user if the app fails to start. ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
     <string name="acknowledge">Concordar</string>
     <string name="decline">Discordar</string>
     <string name="forward_message">Encaminhar mensagem</string>

+ 2 - 0
app/src/main/res/values-ru/strings.xml

@@ -238,6 +238,8 @@
     <string name="prefs_header_other">Другое</string>
     <string name="an_error_occurred">Произошла ошибка</string>
     <string name="an_error_occurred_more">Произошла ошибка: «%1$s»</string>
+    <!-- Error message shown to the user if the app fails to start. ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
+    <string name="startup_error_details">Невозможно запустить ${app_name_short} из-за непредвиденной ошибки.  Экспортируйте файл журнала ниже и свяжитесь с нашей службой поддержки, чтобы мы смогли разобраться в проблеме. Приносим извинения за неудобства.</string>
     <string name="acknowledge">Согласие</string>
     <string name="decline">Несогласие</string>
     <string name="forward_message">Переслать сообщение</string>

+ 1 - 0
app/src/main/res/values-sk/strings.xml

@@ -242,6 +242,7 @@ alebo naskenovať QR kód pomocou iného zariadenia.</string>
     <string name="prefs_header_other">Iné</string>
     <string name="an_error_occurred">Vyskytla sa chyba</string>
     <string name="an_error_occurred_more">Vyskytla sa chyba: “%1$s”</string>
+    <!-- Error message shown to the user if the app fails to start. ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
     <string name="acknowledge">Súhlasím</string>
     <string name="decline">Nesúhlasím</string>
     <string name="forward_message">Preposlať správu</string>

+ 6 - 2
app/src/main/res/values-tr/strings.xml

@@ -239,6 +239,8 @@ tekrar denemeden önce girdiğiniz numaranın doğru olduğundan ve mobil ağa b
     <string name="prefs_header_other">Diğer</string>
     <string name="an_error_occurred">Hata oluştu</string>
     <string name="an_error_occurred_more">Hata oluştu: “%1$s”</string>
+    <!-- Error message shown to the user if the app fails to start. ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
+    <string name="startup_error_details">Beklenmedik bir hata nedeniyle ${app_name_short} açılamadı. Sorunu inceleyebilmemiz için lütfen aşağıdaki günlük dosyasını dışa aktarın ve destek ekibimizle iletişime geçin. Yaşanan aksaklık için özür dileriz.</string>
     <string name="acknowledge">Kabul et</string>
     <string name="decline">Kabul etme</string>
     <string name="forward_message">İlet</string>
@@ -397,7 +399,8 @@ ${app_name_short} uygulamasında silerseniz, kişi senkronizasyonundan sonra tek
     <string name="block_contact">Engelle</string>
     <string name="unblock_contact">Engeli kaldır</string>
     <!-- ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
-    <string name="add_acount_from_within_threema">${app_name_short} uygulamasının içinden ${app_name_short} ID\'nizi yönetin.</string>
+    <string name="add_acount_from_within_threema">
+${app_name_short} uygulamasının içinden ${app_name_short} ID\'nizi yönetin.</string>
     <string name="share_image">Resmi paylaş</string>
     <string name="view_in_gallery">Galeride görüntüle</string>
     <string name="token_register_failed">Push token güncellenemedi, lütfen daha sonra tekrar deneyin.</string>
@@ -1743,7 +1746,8 @@ sunucularımıza güvenli bir şekilde iletildi. Kişisel anahtar hiçbir zaman
     <string name="referral_banner_body">Arkadaşlarınızı şirketlerinde Threema Work\'ü kullanmaya davet edin ve yönlendirme bonusu kazanın!</string>
     <string name="referral_screen_title">Tavsiye Edin ve Faydalanın</string>
     <string name="referral_screen_subtitle">ilk şirketin ilk fatura tutarının %10\'una kadar komisyon kazanın.</string>
-    <string name="referral_how_it_works_title">Nasıl Çalışır?</string>
+    <string name="referral_how_it_works_title">
+Nasıl Çalışır?</string>
     <string name="referral_how_it_works_referral_title">Yönlendirme</string>
     <string name="referral_how_it_works_referral_body">Bir arkadaşınızı veya şirketini Threema Work\'e katılmaya davet edin. Referansınızın dikkate alınabilmesi için kayıt sırasında veya lisans satın alırken Threema kimliğinizi belirtmeniz önemlidir.</string>
     <string name="referral_how_it_works_requirements_title">Gereksinimler</string>

+ 5 - 3
app/src/main/res/values-uk/strings.xml

@@ -33,7 +33,7 @@
     <string name="title_department">Відділ</string>
     <string name="send">Надіслати</string>
     <string name="prefs_linked_devices">Зв\'язані пристрої</string>
-    <string name="prefs_community_and_sharing">Спільнота та спільне використання</string>
+    <string name="prefs_community_and_sharing">Спільнота й посилання</string>
     <!-- Title of section on "Settings" screen, for advanced settings -->
     <string name="prefs_advanced">Розширені налаштування</string>
     <!-- Title of section on "Settings" screen, for information and other options about the app -->
@@ -247,6 +247,8 @@
     <string name="prefs_header_other">Інше</string>
     <string name="an_error_occurred">Сталася помилка</string>
     <string name="an_error_occurred_more">Сталася помилка: \"%1$s\"</string>
+    <!-- Error message shown to the user if the app fails to start. ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
+    <string name="startup_error_details">${app_name_short} не вдалося відкрити через неочікувану помилку. Експортуйте файл журналу нижче та зв\'яжіться з нашою командою підтримки, щоб ми дослідили проблему. Перепрошуємо за незручності.</string>
     <string name="acknowledge">Погоджуюся</string>
     <string name="decline">Не погоджуюся</string>
     <string name="forward_message">Переслати повідомлення</string>
@@ -638,9 +640,9 @@
     <string name="no_app_for_location">На цьому пристрої не знайдено додаток для карт</string>
     <string name="prefs_sendlog">Надіслати журнал</string>
     <!-- ${company_name} is a placeholder for the name of the company that operates the app, e.g. "Threema". It must be left as-is and not translated. -->
-    <string name="prefs_sendlog_summary">Надішліть файл журналу в ${company_name} для аналізу несправності</string>
+    <string name="prefs_sendlog_summary">Надіслати файл журналу в ${company_name} для аналізу несправності</string>
     <string name="prefs_exportlog">Експортувати журнал</string>
-    <string name="prefs_exportlog_summary">Поділіться файлом журналу в іншому додатку</string>
+    <string name="prefs_exportlog_summary">Поділитися файлом журналу в іншому додатку</string>
     <!-- ${app_name} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
     <string name="permission_storage_required">Щоб зберігати та надсилати медіафайли, надайте ${app_name} доступ до сховища.</string>
     <!-- ${app_name} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->

+ 2 - 0
app/src/main/res/values-zh-rCN/strings.xml

@@ -243,6 +243,8 @@
     <string name="prefs_header_other">其他</string>
     <string name="an_error_occurred">发生错误</string>
     <string name="an_error_occurred_more">发生错误:%1$s</string>
+    <!-- Error message shown to the user if the app fails to start. ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
+    <string name="startup_error_details">由于发生意外错误,无法打开 ${app_name_short}。请导出以下的日志文​​件,并联系我们的支持团队,以便我们调查此问题。由此给您带来的不便,我们深表歉意。</string>
     <string name="acknowledge">同意</string>
     <string name="decline">不同意</string>
     <string name="forward_message">转发消息</string>

+ 2 - 0
app/src/main/res/values-zh-rTW/strings.xml

@@ -243,6 +243,8 @@
     <string name="prefs_header_other">其他</string>
     <string name="an_error_occurred">發生錯誤</string>
     <string name="an_error_occurred_more">發生錯誤:%1$s</string>
+    <!-- Error message shown to the user if the app fails to start. ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
+    <string name="startup_error_details">由於發生意外錯誤,無法開啟 ${app_name_short}。請匯出以下的日誌檔案,並聯絡我們的支援團隊,以便我們調查此問題。由此造成您的不便,我們深表歉意。</string>
     <string name="acknowledge">同意</string>
     <string name="decline">不同意</string>
     <string name="forward_message">轉寄訊息</string>

+ 9 - 0
app/src/main/res/values/colors.xml

@@ -80,6 +80,15 @@
     <color name="mention_text_color">@color/md_theme_light_onBackground</color>
     <color name="mention_text_color_inverted">@color/md_theme_light_background</color>
 
+    <!-- Home Screen Widget -->
+    <!-- We need to define them here, as a remote view cannot resolve ?attr/ references -->
+    <color name="widget_color_primary">@color/md_theme_light_primary</color>
+    <color name="widget_color_on_primary">@color/md_theme_light_onPrimary</color>
+    <color name="widget_color_surface_container_low">@color/md_theme_light_surfaceContainerLow</color>
+    <color name="widget_color_on_surface">@color/md_theme_light_onSurface</color>
+    <color name="widget_color_background">@color/md_theme_light_background</color>
+    <color name="widget_color_on_background">@color/md_theme_light_onBackground</color>
+
     <!-- others -->
     <color name="text_color_open_polls">#656565</color>
     <color name="markup_marker_color">#FFD3D3D3</color>

+ 5 - 3
app/src/main/res/values/strings.xml

@@ -246,6 +246,8 @@
     <string name="prefs_header_other">Other</string>
     <string name="an_error_occurred">An error occurred</string>
     <string name="an_error_occurred_more">An error occurred: “%1$s”</string>
+    <!-- Error message shown to the user if the app fails to start. ${app_name_short} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
+    <string name="startup_error_details">${app_name_short} couldn’t be opened because of an unexpected error.  Please export the log file below and contact our support team so we can investigate the issue. We apologize for the inconvenience.</string>
     <string name="acknowledge">Agree</string>
     <string name="decline">Disagree</string>
     <string name="forward_message">Forward message</string>
@@ -636,9 +638,9 @@
     <string name="no_app_for_location">No map application found on this device</string>
     <string name="prefs_sendlog">Send Log</string>
     <!-- ${company_name} is a placeholder for the name of the company that operates the app, e.g. "Threema". It must be left as-is and not translated. -->
-    <string name="prefs_sendlog_summary">Send Logfile to ${company_name} for further analysis in case of problems</string>
-    <string name="prefs_exportlog">Export Log</string>
-    <string name="prefs_exportlog_summary">Share logfile with another app</string>
+    <string name="prefs_sendlog_summary">Send log file to ${company_name} for further analysis in case of problems</string>
+    <string name="prefs_exportlog">Export Log File</string>
+    <string name="prefs_exportlog_summary">Share log file with another app</string>
     <!-- ${app_name} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
     <string name="permission_storage_required">To save or send media, allow ${app_name} permission to access storage.</string>
     <!-- ${app_name} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->

+ 2 - 2
app/src/test/java/ch/threema/app/errorreporting/ErrorReportDetailsProviderTest.kt

@@ -28,7 +28,7 @@ class ErrorReportDetailsProviderTest {
             sentryServiceMetaInfo = SentryService.MetaInfo(
                 deviceModel = "Testing",
                 androidSdkVersion = 35,
-                appVersion = "9.9.9",
+                release = "9.9.9",
                 versionCode = 9,
                 buildFlavor = "TEST",
             ),
@@ -37,7 +37,7 @@ class ErrorReportDetailsProviderTest {
 
         assertEquals(
             """
-                MetaInfo(deviceModel=Testing, androidSdkVersion=35, appVersion=9.9.9, versionCode=9, buildFlavor=TEST)
+                MetaInfo(deviceModel=Testing, androidSdkVersion=35, release=9.9.9, versionCode=9, buildFlavor=TEST)
 
                 Crash
                 java.langRuntimeException: This is a test

+ 1 - 1
app/src/test/java/ch/threema/app/errorreporting/SentryServiceTest.kt

@@ -61,7 +61,7 @@ class SentryServiceTest {
             ),
             metaInfo = SentryService.MetaInfo(
                 androidSdkVersion = 35,
-                appVersion = "6.4.0",
+                release = "test@6.4.0",
                 versionCode = 1234,
                 buildFlavor = "test",
                 deviceModel = "Awesome Phone XY",

+ 2 - 2
app/src/test/resources/api/requests/sentry-envelope-with-exception.txt

@@ -1,3 +1,3 @@
 {"sent_at":"2026-01-26T10:23:20Z"}
-{"type":"event","length":1046}
-{"event_id":"d6d528dff4d04e7d81a53c2404486c39","timestamp":"2026-01-26T10:13:20Z","platform":"android","level":"fatal","release":"6.4.0","dist":"1234","tags":{"android_version":"35","build_flavor":"test"},"user":{"id":"98385cec-e748-468c-9c5e-a30ff6a458d0"},"contexts":{"device":{"name":"Android","model":"Awesome Phone XY"}},"exception":{"values":[{"type":"RuntimeException","value":"An error occurred","module":"ch.threema","mechanism":{"type":"generic","handled":false},"stacktrace":{"frames":[{"filename":"SomeOtherClass.kt","module":"com.example.SomeOtherClass","lineno":42,"raw_function":"fooBar","native":true,"in_app":false},{"filename":"MyClass.kt","module":"ch.threema.MyClass","lineno":67,"raw_function":"testStuff","native":false,"in_app":true}]}},{"type":"IllegalStateException","value":"Another exception","module":"com.example","mechanism":{"type":"generic","handled":false},"stacktrace":{"frames":[{"filename":"TestClass.kt","module":"com.example.TestClass","lineno":1,"raw_function":"testing","native":false,"in_app":false}]}}]}}
+{"type":"event","length":1051}
+{"event_id":"d6d528dff4d04e7d81a53c2404486c39","timestamp":"2026-01-26T10:13:20Z","platform":"android","level":"fatal","release":"test@6.4.0","dist":"1234","tags":{"android_version":"35","build_flavor":"test"},"user":{"id":"98385cec-e748-468c-9c5e-a30ff6a458d0"},"contexts":{"device":{"name":"Android","model":"Awesome Phone XY"}},"exception":{"values":[{"type":"RuntimeException","value":"An error occurred","module":"ch.threema","mechanism":{"type":"generic","handled":false},"stacktrace":{"frames":[{"filename":"SomeOtherClass.kt","module":"com.example.SomeOtherClass","lineno":42,"raw_function":"fooBar","native":true,"in_app":false},{"filename":"MyClass.kt","module":"ch.threema.MyClass","lineno":67,"raw_function":"testStuff","native":false,"in_app":true}]}},{"type":"IllegalStateException","value":"Another exception","module":"com.example","mechanism":{"type":"generic","handled":false},"stacktrace":{"frames":[{"filename":"TestClass.kt","module":"com.example.TestClass","lineno":1,"raw_function":"testing","native":false,"in_app":false}]}}]}}

+ 2 - 2
app/src/test/resources/api/requests/sentry-envelope-with-message.txt

@@ -1,3 +1,3 @@
 {"sent_at":"2026-01-26T10:23:20Z"}
-{"type":"event","length":381}
-{"event_id":"d6d528dff4d04e7d81a53c2404486c39","timestamp":"2026-01-26T10:13:20Z","platform":"android","level":"error","release":"6.4.0","dist":"1234","tags":{"android_version":"35","build_flavor":"test"},"user":{"id":"98385cec-e748-468c-9c5e-a30ff6a458d0"},"contexts":{"device":{"name":"Android","model":"Awesome Phone XY"}},"message":{"message":"This is a %s","params":["test"]}}
+{"type":"event","length":386}
+{"event_id":"d6d528dff4d04e7d81a53c2404486c39","timestamp":"2026-01-26T10:13:20Z","platform":"android","level":"error","release":"test@6.4.0","dist":"1234","tags":{"android_version":"35","build_flavor":"test"},"user":{"id":"98385cec-e748-468c-9c5e-a30ff6a458d0"},"contexts":{"device":{"name":"Android","model":"Awesome Phone XY"}},"message":{"message":"This is a %s","params":["test"]}}

+ 1 - 1
gradle/libs.versions.toml

@@ -17,7 +17,7 @@ androidx_lifecycle = "2.10.0"
 androidx_lifecycleCompose = "2.10.0"
 androidx_lifecycleExtensions = "2.2.0"
 androidx_media = "1.8.0"
-androidx_mmedia3 = "1.10.0"
+androidx_mmedia3 = "1.10.1"
 androidx_pagingRuntime = "3.5.0"
 androidx_palette = "1.0.0"
 androidx_preference = "1.2.1"