Threema 3 месяцев назад
Родитель
Сommit
9bb7408fe3
84 измененных файлов с 1047 добавлено и 653 удалено
  1. 2 2
      app/build.gradle.kts
  2. 3 8
      app/src/libre/play/release-notes/de/default.txt
  3. 3 8
      app/src/libre/play/release-notes/en-US/default.txt
  4. 4 4
      app/src/main/java/ch/threema/app/GlobalListeners.java
  5. 5 1
      app/src/main/java/ch/threema/app/ThreemaApplication.kt
  6. 55 2
      app/src/main/java/ch/threema/app/activities/DirectoryActivity.java
  7. 1 0
      app/src/main/java/ch/threema/app/adapters/ComposeMessageAdapter.java
  8. 3 3
      app/src/main/java/ch/threema/app/adapters/SendMediaPreviewAdapter.kt
  9. 66 53
      app/src/main/java/ch/threema/app/adapters/WidgetViewsFactory.java
  10. 8 4
      app/src/main/java/ch/threema/app/contactdetails/ContactDetailActivity.java
  11. 23 7
      app/src/main/java/ch/threema/app/debug/AndroidContactSyncLogger.kt
  12. 0 1
      app/src/main/java/ch/threema/app/di/DependencyContainer.kt
  13. 12 0
      app/src/main/java/ch/threema/app/di/MasterKeyLockStateChangeHandler.kt
  14. 4 0
      app/src/main/java/ch/threema/app/di/modules/SessionScoped.kt
  15. 2 0
      app/src/main/java/ch/threema/app/di/modules/Utils.kt
  16. 51 50
      app/src/main/java/ch/threema/app/dialogs/PasswordEntryDialog.java
  17. 6 0
      app/src/main/java/ch/threema/app/fragments/ComposeMessageFragment.java
  18. 3 0
      app/src/main/java/ch/threema/app/fragments/ConversationsFragment.kt
  19. 1 1
      app/src/main/java/ch/threema/app/glide/IdentityAvatarFetcher.kt
  20. 5 1
      app/src/main/java/ch/threema/app/processors/incomingcspmessage/conversation/IncomingContactFileMessageTask.kt
  21. 5 1
      app/src/main/java/ch/threema/app/processors/incomingcspmessage/conversation/IncomingGroupFileMessageTask.kt
  22. 2 2
      app/src/main/java/ch/threema/app/routines/SynchronizeContactsRoutine.java
  23. 4 3
      app/src/main/java/ch/threema/app/services/ContactServiceImpl.java
  24. 5 4
      app/src/main/java/ch/threema/app/services/FileServiceImpl.java
  25. 15 15
      app/src/main/java/ch/threema/app/services/PinLockService.java
  26. 0 100
      app/src/main/java/ch/threema/app/services/WidgetService.java
  27. 31 0
      app/src/main/java/ch/threema/app/services/WidgetService.kt
  28. 5 6
      app/src/main/java/ch/threema/app/services/license/LicenseServiceSerial.java
  29. 0 8
      app/src/main/java/ch/threema/app/services/license/LicenseServiceThreema.java
  30. 4 4
      app/src/main/java/ch/threema/app/services/license/LicenseServiceUser.java
  31. 1 1
      app/src/main/java/ch/threema/app/services/messageplayer/AudioMessagePlayer.java
  32. 7 13
      app/src/main/java/ch/threema/app/stores/EncryptedPreferenceStoreImpl.kt
  33. 3 0
      app/src/main/java/ch/threema/app/systemupdates/updates/SystemUpdateToVersion113.kt
  34. 5 1
      app/src/main/java/ch/threema/app/ui/DirectoryDataSourceFactory.java
  35. 7 5
      app/src/main/java/ch/threema/app/usecases/conversations/WatchConversationListItemsUseCase.kt
  36. 57 22
      app/src/main/java/ch/threema/app/utils/AndroidContactUtil.java
  37. 5 0
      app/src/main/java/ch/threema/app/utils/IntentDataUtil.java
  38. 1 0
      app/src/main/java/ch/threema/app/utils/NameUtil.java
  39. 5 18
      app/src/main/java/ch/threema/app/utils/ShortcutUtil.java
  40. 5 2
      app/src/main/java/ch/threema/app/utils/TestUtil.java
  41. 0 47
      app/src/main/java/ch/threema/app/utils/WidgetUtil.java
  42. 37 0
      app/src/main/java/ch/threema/app/utils/WidgetUtil.kt
  43. 17 18
      app/src/main/java/ch/threema/app/workers/ShareTargetUpdateWorker.kt
  44. 53 0
      app/src/main/java/ch/threema/base/FileExtensions.kt
  45. 1 0
      app/src/main/java/ch/threema/localcrypto/MasterKeyImpl.kt
  46. 11 0
      app/src/main/java/ch/threema/localcrypto/MasterKeyStorageManager.kt
  47. 1 1
      app/src/main/java/ch/threema/localcrypto/Version1MasterKeyFileManager.kt
  48. 2 18
      app/src/main/java/ch/threema/localcrypto/Version2MasterKeyFileManager.kt
  49. 4 1
      app/src/main/res/layout/activity_unlock_masterkey.xml
  50. 1 0
      app/src/main/res/layout/dialog_password_entry.xml
  51. 11 11
      app/src/main/res/values-be-rBY/strings.xml
  52. 10 10
      app/src/main/res/values-bg/strings.xml
  53. 9 9
      app/src/main/res/values-ca/strings.xml
  54. 10 9
      app/src/main/res/values-cs/strings.xml
  55. 10 9
      app/src/main/res/values-de/strings.xml
  56. 14 13
      app/src/main/res/values-es/strings.xml
  57. 1 1
      app/src/main/res/values-es/webclient_strings.xml
  58. 11 10
      app/src/main/res/values-fr/strings.xml
  59. 38 10
      app/src/main/res/values-gsw/strings.xml
  60. 2 0
      app/src/main/res/values-gsw/voip_strings.xml
  61. 9 9
      app/src/main/res/values-hu/strings.xml
  62. 10 9
      app/src/main/res/values-it/strings.xml
  63. 17 9
      app/src/main/res/values-ja/strings.xml
  64. 1 0
      app/src/main/res/values-ja/voip_strings.xml
  65. 11 10
      app/src/main/res/values-nl-rNL/strings.xml
  66. 9 9
      app/src/main/res/values-no/strings.xml
  67. 11 10
      app/src/main/res/values-pl/strings.xml
  68. 14 13
      app/src/main/res/values-pt-rBR/strings.xml
  69. 1 1
      app/src/main/res/values-pt-rBR/webclient_strings.xml
  70. 11 10
      app/src/main/res/values-ru/strings.xml
  71. 39 11
      app/src/main/res/values-sk/strings.xml
  72. 2 0
      app/src/main/res/values-sk/voip_strings.xml
  73. 2 0
      app/src/main/res/values-sk/webclient_strings.xml
  74. 11 10
      app/src/main/res/values-tr/strings.xml
  75. 9 8
      app/src/main/res/values-uk/strings.xml
  76. 11 10
      app/src/main/res/values-zh-rCN/strings.xml
  77. 11 10
      app/src/main/res/values-zh-rTW/strings.xml
  78. 2 1
      app/src/main/res/values/strings.xml
  79. 1 2
      app/src/main/res/values/untranslatable_strings.xml
  80. 74 0
      app/src/test/java/ch/threema/base/FileExtensionsTest.kt
  81. 81 3
      app/src/test/java/ch/threema/localcrypto/MasterKeyStorageManagerTest.kt
  82. 34 0
      common/src/main/java/ch/threema/common/NoCloseOutputStream.kt
  83. 1 1
      gradle/libs.versions.toml
  84. 8 0
      test-helpers/src/main/java/ch/threema/testhelpers/TestHelpers.kt

+ 2 - 2
app/build.gradle.kts

@@ -51,7 +51,7 @@ if (gradle.startParameter.taskRequests.toString().contains("Hms")) {
 /**
  * Only use the scheme "<major>.<minor>.<patch>" for the appVersion
  */
-val appVersion = "6.2.0"
+val appVersion = "6.2.1"
 
 /**
  * betaSuffix with leading dash (e.g. `-beta1`).
@@ -60,7 +60,7 @@ val appVersion = "6.2.0"
  */
 val betaSuffix = ""
 
-val defaultVersionCode = 1095
+val defaultVersionCode = 1098
 
 /**
  * Map with keystore paths (if found).

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

@@ -1,8 +1,3 @@
-- Android 5 und 6 werden nicht mehr unterstützt
-- Anhebung des Android-SDK-Levels auf 35 (Android 15)
-- Unterstützung für Seitengrössen von 16 KB
-- Unterstützung von Emoji v16.0
-- Verwendung von «libthreema» für kryptografische Vorgänge
-- Behebung eines Fehlers bei der Aufnahme von Videos
-- Verschiedene Farb- und UI-Verbesserungen
-- Verbesserungen und Behebung verschiedener Fehler
+- Behebung eines Fehlers bei der Verwendung von Startbildschirm-Verknüpfungen
+- Behebung eines Fehlers, wodurch die App beim Öffnen des Archivs abstürzen konnte
+- Verschiedene UI-Verbesserungen und weitere kleine Fehlerbehebungen

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

@@ -1,8 +1,3 @@
-- Android 5 and 6 are no longer supported
-- Increased the Android SDK level to 35 (Android 15)
-- Support for 16 KB page sizes
-- Support for emoji v16.0
-- Use “libthreema” for cryptographic operations
-- Fixed a bug that could occur when recording a video
-- Various color and UI improvements
-- Various improvements and bug fixes
+- Fixed a bug that could occur when using home screen shortcuts
+- Fixed a bug that could cause the app to crash when opening the archive
+- Various UI improvements and small bug fixes

+ 4 - 4
app/src/main/java/ch/threema/app/GlobalListeners.java

@@ -232,7 +232,7 @@ public class GlobalListeners {
                     }
 
                     // update widget on incoming message
-                    WidgetUtil.updateWidgets(serviceManager.getContext());
+                    WidgetUtil.updateWidgets(appContext);
                 }
             }
         } catch (ThreemaException e) {
@@ -841,13 +841,13 @@ public class GlobalListeners {
         @Override
         public void onStarted(SynchronizeContactsRoutine startedRoutine) {
             //disable contact observer
-            serviceManager.getContext().getContentResolver().unregisterContentObserver(contentObserverChangeContactNames);
+            appContext.getContentResolver().unregisterContentObserver(contentObserverChangeContactNames);
         }
 
         @Override
         public void onFinished(SynchronizeContactsRoutine finishedRoutine) {
             //enable contact observer
-            serviceManager.getContext().getContentResolver().registerContentObserver(
+            appContext.getContentResolver().registerContentObserver(
                 ContactsContract.Contacts.CONTENT_URI,
                 false,
                 contentObserverChangeContactNames);
@@ -856,7 +856,7 @@ public class GlobalListeners {
         @Override
         public void onError(SynchronizeContactsRoutine finishedRoutine) {
             //enable contact observer
-            serviceManager.getContext().getContentResolver().registerContentObserver(
+            appContext.getContentResolver().registerContentObserver(
                 ContactsContract.Contacts.CONTENT_URI,
                 false,
                 contentObserverChangeContactNames);

+ 5 - 1
app/src/main/java/ch/threema/app/ThreemaApplication.kt

@@ -62,6 +62,7 @@ import ch.threema.app.startup.RemoteSecretMonitorRetryController
 import ch.threema.app.startup.models.AppSystem
 import ch.threema.app.stores.EncryptedPreferenceStore
 import ch.threema.app.stores.EncryptedPreferenceStoreImpl
+import ch.threema.app.stores.IdentityProvider
 import ch.threema.app.stores.IdentityStoreImpl
 import ch.threema.app.stores.MutableIdentityProvider
 import ch.threema.app.stores.PreferenceStore
@@ -136,6 +137,7 @@ open class ThreemaApplication : Application() {
     private val masterKeyEventMonitor: MasterKeyEventMonitor by inject()
     private val appTaskExecutor: AppTaskExecutor by inject()
     private val appStartupMonitor: AppStartupMonitorImpl by inject()
+    private val identityProvider: IdentityProvider by inject()
 
     override fun onCreate() {
         if (!checkAppReplacingState(applicationContext)) {
@@ -162,10 +164,12 @@ open class ThreemaApplication : Application() {
         logger.info("*** App launched")
         logAppVersionInfo(applicationContext)
         logExitReason(applicationContext, PreferenceManager.getDefaultSharedPreferences(applicationContext))
-        logger.info("Remote secrets supported: {}", ConfigUtils.isRemoteSecretsSupported())
 
         initDependencyInjection(this)
 
+        logger.info("Has identity: {}", identityProvider.getIdentity() != null)
+        logger.info("Remote secrets supported: {}", ConfigUtils.isRemoteSecretsSupported())
+
         ProcessLifecycleOwner.get().lifecycle.addObserver(get<AppProcessLifecycleObserver>())
 
         val masterKeyManager: MasterKeyManagerImpl = try {

+ 55 - 2
app/src/main/java/ch/threema/app/activities/DirectoryActivity.java

@@ -41,6 +41,8 @@ import com.google.android.material.chip.ChipGroup;
 import com.google.android.material.progressindicator.LinearProgressIndicator;
 import com.google.android.material.search.SearchBar;
 
+import org.json.JSONException;
+import org.json.JSONObject;
 import org.koin.java.KoinJavaComponent;
 import org.slf4j.Logger;
 
@@ -56,6 +58,7 @@ import androidx.annotation.NonNull;
 import androidx.annotation.UiThread;
 import androidx.appcompat.app.ActionBar;
 import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
 import androidx.paging.LivePagedListBuilder;
 import androidx.paging.PagedList;
 import androidx.recyclerview.widget.DefaultItemAnimator;
@@ -65,6 +68,7 @@ import ch.threema.app.adapters.DirectoryAdapter;
 import ch.threema.app.asynctasks.AddOrUpdateWorkContactBackgroundTask;
 import ch.threema.app.di.DependencyContainer;
 import ch.threema.app.dialogs.MultiChoiceSelectorDialog;
+import ch.threema.app.ui.DirectoryDataSource;
 import ch.threema.app.ui.DirectoryDataSourceFactory;
 import ch.threema.app.ui.DirectoryHeaderItemDecoration;
 import ch.threema.app.ui.EmptyRecyclerView;
@@ -88,6 +92,9 @@ import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 public class DirectoryActivity extends ThreemaToolbarActivity implements ThreemaSearchView.OnQueryTextListener, MultiChoiceSelectorDialog.SelectorDialogClickListener {
     private static final Logger logger = LoggingUtil.getThreemaLogger("DirectoryActivity");
 
+    private static final String EXTRA_QUERY_TEXT = "queryText";
+    private static final String EXTRA_CHECKED_CATEGORIES = "checkedCategories";
+
     private static final int API_DIRECTORY_PAGE_SIZE = 3;
     private static final long QUERY_TIMEOUT = 1000; // ms
     private static final String DIALOG_TAG_CATEGORY_SELECTOR = "cs";
@@ -278,6 +285,23 @@ public class DirectoryActivity extends ThreemaToolbarActivity implements Threema
         });
 
         recyclerView.setAdapter(directoryAdapter);
+
+        if (savedInstanceState != null) {
+            queryText = savedInstanceState.getString(EXTRA_QUERY_TEXT);
+            String[] checkedCategoriesJson = savedInstanceState.getStringArray(EXTRA_CHECKED_CATEGORIES);
+            if (checkedCategoriesJson != null) {
+                checkedCategories.clear();
+                for (String checkedCategoryJson : checkedCategoriesJson) {
+                    try {
+                        checkedCategories.add(new WorkDirectoryCategory(new JSONObject(checkedCategoryJson)));
+                    } catch (JSONException e) {
+                        logger.error("Could not restore category", e);
+                    }
+                }
+                updateSelectedCategories();
+            }
+        }
+
         return true;
     }
 
@@ -341,6 +365,13 @@ public class DirectoryActivity extends ThreemaToolbarActivity implements Threema
                             searchBar.findViewById(R.id.menu_category).getLocationInWindow(locationCategoryIcon);
                             searchBar.getTextView().getLocationInWindow(locationTextView);
                             searchView.setMaxWidth(locationCategoryIcon[0] - locationTextView[0]);
+
+                            // The query text might already be set in case the activity is recreated
+                            if (queryText != null && !queryText.isEmpty()) {
+                                searchView.setQuery(queryText, true);
+                                // This needs to be set for the text to be visible
+                                searchView.setIconified(false);
+                            }
                         } catch (Exception e) {
                             logger.debug("Unable to patch searchview");
                         }
@@ -507,8 +538,16 @@ public class DirectoryActivity extends ThreemaToolbarActivity implements Threema
 
     private void updateDirectory() {
         updateEmptyViewState(EMPTY_STATE_SEARCHING);
-        directoryDataSourceFactory.postLiveData.getValue().setQueryCategories(checkedCategories);
-        directoryDataSourceFactory.postLiveData.getValue().invalidate();
+        MutableLiveData<DirectoryDataSource> postLiveData = directoryDataSourceFactory.postLiveData;
+        if (postLiveData == null) {
+            return;
+        }
+        DirectoryDataSource dataSource = postLiveData.getValue();
+        if (dataSource == null) {
+            return;
+        }
+        dataSource.setQueryCategories(checkedCategories);
+        dataSource.invalidate();
     }
 
     private void updateEmptyViewState(@EmptyState int newState) {
@@ -559,6 +598,20 @@ public class DirectoryActivity extends ThreemaToolbarActivity implements Threema
         updateSelectedCategories();
     }
 
+    @Override
+    protected void onSaveInstanceState(@NonNull Bundle outState) {
+        super.onSaveInstanceState(outState);
+
+        outState.putString(EXTRA_QUERY_TEXT, queryText);
+        String[] checkedCategoriesJsons = checkedCategories
+            .stream()
+            .map(WorkDirectoryCategory::toJSON)
+            .toArray(String[]::new);
+        if (checkedCategoriesJsons.length > 0) {
+            outState.putStringArray(EXTRA_CHECKED_CATEGORIES, checkedCategoriesJsons);
+        }
+    }
+
     @Override
     public void onConfigurationChanged(@NonNull Configuration newConfig) {
         super.onConfigurationChanged(newConfig);

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

@@ -1171,6 +1171,7 @@ public class ComposeMessageAdapter extends ArrayAdapter<AbstractMessageModel> im
             AbstractMessageModel nextMessage = values.get(index + 1);
             if (
                 nextMessage != null &&
+                    nextMessage.getId() != messageModel.getId() &&
                     nextMessage.isDownloadedVoiceMessage() &&
                     messageModel.isOutbox() == nextMessage.isOutbox() &&
                     messageModel.isAvailable()

+ 3 - 3
app/src/main/java/ch/threema/app/adapters/SendMediaPreviewAdapter.kt

@@ -242,10 +242,10 @@ class SendMediaPreviewAdapter(
                 }
 
                 override fun onResourceReady(
-                    resource: Drawable,
-                    model: Any,
+                    resource: Drawable?,
+                    model: Any?,
                     target: Target<Drawable?>?,
-                    dataSource: DataSource,
+                    dataSource: DataSource?,
                     isFirstResource: Boolean,
                 ): Boolean {
                     setQualifierView(item, holder)

+ 66 - 53
app/src/main/java/ch/threema/app/adapters/WidgetViewsFactory.java

@@ -28,8 +28,10 @@ import android.os.Bundle;
 import android.widget.RemoteViews;
 import android.widget.RemoteViewsService;
 
+import org.koin.java.KoinJavaComponent;
 import org.slf4j.Logger;
 
+import java.util.Collections;
 import java.util.List;
 
 import androidx.annotation.NonNull;
@@ -37,7 +39,6 @@ import androidx.annotation.Nullable;
 import ch.threema.app.AppConstants;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
-import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.services.ContactService;
 import ch.threema.app.services.ConversationCategoryService;
 import ch.threema.app.services.ConversationService;
@@ -48,7 +49,6 @@ import ch.threema.app.services.MessageService;
 import ch.threema.app.services.NotificationPreferenceService;
 import ch.threema.app.preference.service.PreferenceService;
 import ch.threema.app.utils.MessageUtil;
-import ch.threema.base.ThreemaException;
 import ch.threema.base.utils.LoggingUtil;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.ContactModel;
@@ -59,46 +59,13 @@ public class WidgetViewsFactory implements RemoteViewsService.RemoteViewsFactory
 
     @NonNull
     private final Context context;
-    @NonNull
-    private final ConversationService conversationService;
-    @NonNull
-    private final GroupService groupService;
-    @NonNull
-    private final ContactService contactService;
-    @NonNull
-    private final DistributionListService distributionListService;
-    @NonNull
-    private final LockAppService lockAppService;
-    @NonNull
-    private final PreferenceService preferenceService;
-    @NonNull
-    private final NotificationPreferenceService notificationPreferenceService;
-    @NonNull
-    private final MessageService messageService;
-    @NonNull
-    private final ConversationCategoryService conversationCategoryService;
 
     private List<ConversationModel> conversations;
 
-    public WidgetViewsFactory(@NonNull Context context) throws ThreemaException {
+    public WidgetViewsFactory(@NonNull Context context) {
         this.context = context;
-
-        ServiceManager serviceManager = ThreemaApplication.getServiceManager();
-        if (serviceManager == null) {
-            throw new ThreemaException("Could not get the service manager");
-        }
-        this.conversationService = serviceManager.getConversationService();
-        this.contactService = serviceManager.getContactService();
-        this.groupService = serviceManager.getGroupService();
-        this.distributionListService = serviceManager.getDistributionListService();
-        this.messageService = serviceManager.getMessageService();
-        this.lockAppService = serviceManager.getLockAppService();
-        this.preferenceService = serviceManager.getPreferenceService();
-        this.notificationPreferenceService = serviceManager.getNotificationPreferenceService();
-        this.conversationCategoryService = serviceManager.getConversationCategoryService();
     }
 
-
     /**
      * Called when your factory is first constructed. The same factory may be shared across
      * multiple RemoteViewAdapters depending on the intent passed.
@@ -121,19 +88,33 @@ public class WidgetViewsFactory implements RemoteViewsService.RemoteViewsFactory
      */
     @Override
     public void onDataSetChanged() {
-        conversations = conversationService.getAll(false, new ConversationService.Filter() {
-            @Override
-            public boolean onlyUnread() {
-                return true;
+        logger.info("onDataSetChanged called");
+        // TODO(ANDR-4267): Improve dependency injection
+        try {
+            if (ThreemaApplication.getServiceManager() == null) {
+                logger.info("Service manager is unavailable, don't show anything");
+                conversations = Collections.emptyList();
+                return;
             }
+            ConversationService conversationService = KoinJavaComponent.get(ConversationService.class);
+            PreferenceService preferenceService = KoinJavaComponent.get(PreferenceService.class);
+            conversations = conversationService.getAll(false, new ConversationService.Filter() {
+                @Override
+                public boolean onlyUnread() {
+                    return true;
+                }
 
-            @Override
-            public boolean noHiddenChats() {
-                return preferenceService.isPrivateChatsHidden();
-            }
+                @Override
+                public boolean noHiddenChats() {
+                    return preferenceService.isPrivateChatsHidden();
+                }
 
-        });
-        logger.info("Conversations updated");
+            });
+            logger.info("Conversations updated");
+        } catch (Exception e) {
+            logger.error("Failed to get conversations for widget", e);
+            conversations = Collections.emptyList();
+        }
     }
 
     /**
@@ -150,12 +131,22 @@ public class WidgetViewsFactory implements RemoteViewsService.RemoteViewsFactory
      */
     @Override
     public int getCount() {
-        if (!lockAppService.isLocked() &&
-            notificationPreferenceService.isShowMessagePreview() &&
-            conversations != null
-        ) {
-            return conversations.size();
-        } else {
+        // TODO(ANDR-4267): Improve dependency injection
+        try {
+            if (ThreemaApplication.getServiceManager() == null) {
+                logger.info("Service manager is unavailable, setting count to 0");
+                return 0;
+            }
+            LockAppService lockAppService = KoinJavaComponent.get(LockAppService.class);
+            NotificationPreferenceService notificationPreferenceService = KoinJavaComponent.get(NotificationPreferenceService.class);
+
+            if (!lockAppService.isLocked() && notificationPreferenceService.isShowMessagePreview() && conversations != null) {
+                return conversations.size();
+            } else {
+                return 0;
+            }
+        } catch (Exception e) {
+            logger.error("Failed to get count for widget", e);
             return 0;
         }
     }
@@ -170,6 +161,28 @@ public class WidgetViewsFactory implements RemoteViewsService.RemoteViewsFactory
      */
     @Override
     public RemoteViews getViewAt(int position) {
+        // TODO(ANDR-4267): Improve dependency injection
+        LockAppService lockAppService;
+        NotificationPreferenceService notificationPreferenceService;
+        ContactService contactService;
+        GroupService groupService;
+        DistributionListService distributionListService;
+        ConversationCategoryService conversationCategoryService;
+        MessageService messageService;
+
+        try {
+            lockAppService = KoinJavaComponent.get(LockAppService.class);
+            notificationPreferenceService = KoinJavaComponent.get(NotificationPreferenceService.class);
+            contactService = KoinJavaComponent.get(ContactService.class);
+            groupService = KoinJavaComponent.get(GroupService.class);
+            distributionListService = KoinJavaComponent.get(DistributionListService.class);
+            conversationCategoryService = KoinJavaComponent.get(ConversationCategoryService.class);
+            messageService = KoinJavaComponent.get(MessageService.class);
+        } catch (Exception e) {
+            logger.error("Failed to get dependencies for updating view in widget", e);
+            return null;
+        }
+
         if (conversations != null && !conversations.isEmpty() && position < conversations.size()) {
             ConversationModel conversationModel = conversations.get(position);
 
@@ -180,7 +193,7 @@ public class WidgetViewsFactory implements RemoteViewsService.RemoteViewsFactory
                 Bundle extras = new Bundle();
                 String uniqueId = conversationModel.messageReceiver.getUniqueIdString();
 
-                if (!this.lockAppService.isLocked() && notificationPreferenceService.isShowMessagePreview()) {
+                if (!lockAppService.isLocked() && notificationPreferenceService.isShowMessagePreview()) {
                     sender = conversationModel.messageReceiver.getDisplayName();
 
                     if (conversationModel.isContactConversation()) {

+ 8 - 4
app/src/main/java/ch/threema/app/contactdetails/ContactDetailActivity.java

@@ -123,6 +123,7 @@ import ch.threema.data.models.GroupIdentity;
 import ch.threema.domain.models.VerificationLevel;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.GroupModel;
+import kotlin.Lazy;
 
 import static ch.threema.app.utils.QRScannerUtil.REQUEST_CODE_QR_SCANNER;
 import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
@@ -150,6 +151,9 @@ public class ContactDetailActivity extends ThreemaToolbarActivity
     @NonNull
     private final DependencyContainer dependencies = KoinJavaComponent.get(DependencyContainer.class);
 
+    @NonNull
+    private final Lazy<AndroidContactUtil> androidContactUtil = KoinJavaComponent.inject(AndroidContactUtil.class);
+
     private final @NonNull LazyProperty<BackgroundExecutor> backgroundExecutor = new LazyProperty<>(BackgroundExecutor::new);
 
     // Data and state holders
@@ -634,7 +638,7 @@ public class ContactDetailActivity extends ThreemaToolbarActivity
 
     private void openContactEditor() {
         if (contactModel != null) {
-            if (!AndroidContactUtil.getInstance().openContactEditor(this, contactModel, REQUEST_CODE_CONTACT_EDITOR)) {
+            if (!androidContactUtil.getValue().openContactEditor(this, contactModel, REQUEST_CODE_CONTACT_EDITOR)) {
                 editName();
             }
         }
@@ -675,7 +679,7 @@ public class ContactDetailActivity extends ThreemaToolbarActivity
                     dependencies.getWallpaperService(),
                     dependencies.getFileService(),
                     dependencies.getExcludedSyncIdentitiesService(),
-                    dependencies.getDhSessionStoreInterface(),
+                    dependencies.getDhSessionStore(),
                     dependencies.getNotificationService(),
                     dependencies.getDatabaseService()
                 ),
@@ -919,8 +923,8 @@ public class ContactDetailActivity extends ThreemaToolbarActivity
                 break;
             case REQUEST_CODE_CONTACT_EDITOR:
                 try {
-                    AndroidContactUtil.getInstance().updateNameByAndroidContact(contactModel, null);
-                    AndroidContactUtil.getInstance().updateAvatarByAndroidContact(contactModel);
+                    androidContactUtil.getValue().updateNameByAndroidContact(contactModel, null, this);
+                    androidContactUtil.getValue().updateAvatarByAndroidContact(contactModel, this);
                     this.avatarEditView.setContactIdentity(contactModel.getIdentity());
                 } catch (ThreemaException | SecurityException e) {
                     logger.info("Unable to update contact name or avatar after returning from ContactEditor");

+ 23 - 7
app/src/main/java/ch/threema/app/debug/AndroidContactSyncLogger.kt

@@ -21,7 +21,6 @@
 
 package ch.threema.app.debug
 
-import android.net.Uri
 import ch.threema.base.utils.LoggingUtil
 
 private val logger = LoggingUtil.getThreemaLogger("AndroidContactSyncLogger")
@@ -30,13 +29,13 @@ private val logger = LoggingUtil.getThreemaLogger("AndroidContactSyncLogger")
  * This logger is used to count the number of identical contacts when importing them from the android contacts.
  */
 class AndroidContactSyncLogger {
-    private val nameMap = mutableMapOf<Pair<String, String>, MutableList<Uri>>()
+    private val nameMap = mutableMapOf<Pair<String, String>, MutableList<LookupKeyAndContactId>>()
 
     /**
-     * Add the first and last name that was received for a certain uri to the logger.
+     * Add the first and last name that was received for a certain lookup key to the logger.
      */
-    fun addSyncedNames(uri: Uri, firstName: String?, lastName: String?) {
-        nameMap.getOrPut(firstName.orEmpty() to lastName.orEmpty()) { mutableListOf() }.add(uri)
+    fun addSyncedNames(lookupKeyAndContactId: LookupKeyAndContactId, firstName: String?, lastName: String?) {
+        nameMap.getOrPut(firstName.orEmpty() to lastName.orEmpty()) { mutableListOf() }.add(lookupKeyAndContactId)
     }
 
     /**
@@ -44,8 +43,20 @@ class AndroidContactSyncLogger {
      */
     fun logDuplicates() {
         val duplicateNames = nameMap.values.filter { it.size > 1 }
-        duplicateNames.forEach { uriList ->
-            logger.warn("Got the same name for {} contacts (for {} different uris)", uriList.size, uriList.map { it.path }.toSet().size)
+        duplicateNames.forEach { lookupKeysAndContactId ->
+            // The number of different combinations of lookup key and contact id
+            val lookupKeyAndContactIdCombinations = lookupKeysAndContactId.toSet().size
+            // The number of different lookup keys
+            val lookupKeys = lookupKeysAndContactId.map(LookupKeyAndContactId::lookupKey).toSet().size
+            // The number of different contact ids
+            val contactIds = lookupKeysAndContactId.map(LookupKeyAndContactId::contactId).toSet().size
+            logger.warn(
+                "Got the same name for {} contacts (with {} lookup key and contact id combinations; {} lookup keys; {} contact ids)",
+                lookupKeysAndContactId.size,
+                lookupKeyAndContactIdCombinations,
+                lookupKeys,
+                contactIds,
+            )
         }
 
         if (duplicateNames.isEmpty()) {
@@ -53,3 +64,8 @@ class AndroidContactSyncLogger {
         }
     }
 }
+
+data class LookupKeyAndContactId(
+    val lookupKey: String,
+    val contactId: Long,
+)

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

@@ -104,7 +104,6 @@ class DependencyContainer : KoinComponent {
     val databaseService: DatabaseService by inject()
     val deviceService: DeviceService by inject()
     val dhSessionStore: DHSessionStoreInterface by inject()
-    val dhSessionStoreInterface: DHSessionStoreInterface by inject()
     val distributionListService: DistributionListService by inject()
     val emojiService: EmojiService by inject()
     val excludedSyncIdentitiesService: ExcludedSyncIdentitiesService by inject()

+ 12 - 0
app/src/main/java/ch/threema/app/di/MasterKeyLockStateChangeHandler.kt

@@ -29,6 +29,9 @@ import ch.threema.app.startup.AppStartupMonitorImpl
 import ch.threema.app.systemupdates.SystemUpdateState
 import ch.threema.app.utils.ConfigUtils
 import ch.threema.app.utils.DispatcherProvider
+import ch.threema.app.utils.ShortcutUtil
+import ch.threema.app.utils.WidgetUtil
+import ch.threema.app.workers.ShareTargetUpdateWorker
 import ch.threema.base.utils.LoggingUtil
 import ch.threema.storage.DatabaseState
 import kotlinx.coroutines.coroutineScope
@@ -59,6 +62,8 @@ class MasterKeyLockStateChangeHandler(
         globalListeners = GlobalListeners(appContext, serviceManager).apply {
             setUp()
         }
+
+        WidgetUtil.updateWidgets(appContext)
     }
 
     suspend fun onMasterKeyLocked() = coroutineScope {
@@ -78,6 +83,11 @@ class MasterKeyLockStateChangeHandler(
                 }
             }
 
+            if (serviceManager.preferenceService.isDirectShare) {
+                ShareTargetUpdateWorker.cancelScheduledShareTargetShortcutUpdate(appContext)
+                ShortcutUtil.deleteAllShareTargetShortcuts()
+            }
+
             launch(dispatcherProvider.io) {
                 serviceManager.databaseService.close()
                 serviceManager.dhSessionStore.close()
@@ -88,5 +98,7 @@ class MasterKeyLockStateChangeHandler(
         globalListeners = null
 
         ConfigUtils.scheduleAppRestart(appContext)
+
+        WidgetUtil.updateWidgets(appContext)
     }
 }

+ 4 - 0
app/src/main/java/ch/threema/app/di/modules/SessionScoped.kt

@@ -25,8 +25,10 @@ import ch.threema.app.di.DependencyContainer
 import ch.threema.app.di.repository
 import ch.threema.app.di.service
 import ch.threema.app.managers.ServiceManager
+import ch.threema.app.services.ExcludedSyncIdentitiesService
 import ch.threema.app.services.ServerAddressProviderService
 import ch.threema.app.services.ServiceManagerProvider
+import ch.threema.app.services.WallpaperService
 import ch.threema.app.webclient.manager.WebClientServiceManager
 import ch.threema.localcrypto.MasterKeyManager
 import org.koin.core.module.dsl.factoryOf
@@ -58,6 +60,7 @@ val sessionScopedModule = module {
     service { distributionListService }
     service { emojiService }
     service { encryptedPreferenceStore }
+    service<ExcludedSyncIdentitiesService> { excludedSyncIdentitiesService }
     service { fileService }
     service { groupCallManager }
     service { groupCallManager }
@@ -86,6 +89,7 @@ val sessionScopedModule = module {
     service { threemaSafeService }
     service { userService }
     service { voipStateService }
+    service<WallpaperService> { wallpaperService }
     service { webClientServiceManager }
 
     repository { contacts }

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

@@ -22,6 +22,7 @@
 package ch.threema.app.di.modules
 
 import androidx.preference.PreferenceManager
+import ch.threema.app.utils.AndroidContactUtil
 import ch.threema.app.utils.DispatcherProvider
 import ch.threema.app.utils.Toaster
 import org.koin.core.module.dsl.factoryOf
@@ -33,6 +34,7 @@ import org.koin.dsl.module
  * not hold global state.
  */
 val utilsModule = module {
+    factory { AndroidContactUtil.getInstance() }
     factory { DispatcherProvider.default }
     factoryOf(::Toaster)
     factory { PreferenceManager.getDefaultSharedPreferences(get()) }

+ 51 - 50
app/src/main/java/ch/threema/app/dialogs/PasswordEntryDialog.java

@@ -34,7 +34,6 @@ import android.text.method.LinkMovementMethod;
 import android.text.util.Linkify;
 import android.view.View;
 import android.view.WindowManager;
-import android.widget.EditText;
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
@@ -66,7 +65,7 @@ public class PasswordEntryDialog extends ThreemaDialogFragment implements Generi
     protected Activity activity;
     protected AlertDialog alertDialog;
     protected boolean isLinkify = false;
-    protected boolean isLengthCheck = true;
+    protected boolean requiresPasswordConfirmation = true;
     protected int minLength, maxLength;
     protected MaterialSwitch checkBox;
 
@@ -201,8 +200,14 @@ public class PasswordEntryDialog extends ThreemaDialogFragment implements Generi
         final TextInputLayout editText2Layout = dialogView.findViewById(R.id.password2layout);
         checkBox = dialogView.findViewById(R.id.check_box);
 
-        editText1.addTextChangedListener(new PasswordWatcher(editText1, editText2));
-        editText2.addTextChangedListener(new PasswordWatcher(editText1, editText2));
+        var passwordWatcher = new SimpleTextWatcher() {
+            @Override
+            public void afterTextChanged(@NonNull Editable editable) {
+                updateViews();
+            }
+        };
+        editText1.addTextChangedListener(passwordWatcher);
+        editText2.addTextChangedListener(passwordWatcher);
 
         if (maxLength > 0) {
             editText1.setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxLength)});
@@ -253,7 +258,7 @@ public class PasswordEntryDialog extends ThreemaDialogFragment implements Generi
             editText1.setInputType(inputTypePasswordHidden);
             editText2.setVisibility(View.GONE);
             editText2Layout.setVisibility(View.GONE);
-            isLengthCheck = false;
+            requiresPasswordConfirmation = false;
         } else {
             editText2Layout.setHint(getString(confirmHint));
             editText1Layout.setHelperTextEnabled(true);
@@ -312,38 +317,6 @@ public class PasswordEntryDialog extends ThreemaDialogFragment implements Generi
         callback.onNo(this.getTag());
     }
 
-    public class PasswordWatcher extends SimpleTextWatcher {
-        private final EditText password1;
-        private final EditText password2;
-
-        public PasswordWatcher(final EditText password1, final EditText password2) {
-            this.password1 = password1;
-            this.password2 = password2;
-        }
-
-        @Override
-        public void afterTextChanged(@NonNull Editable editable) {
-            String password1Text = password1.getText().toString();
-            String password2Text = password2.getText().toString();
-
-            if (isLengthCheck) {
-                alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(getPasswordOK(password1Text, password2Text));
-            } else {
-                alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(!password1Text.isEmpty());
-            }
-        }
-    }
-
-    private boolean getPasswordOK(String password1Text, String password2Text) {
-        boolean lengthOk = password1Text.length() >= minLength;
-        if (maxLength > 0) {
-            lengthOk = lengthOk && password1Text.length() <= maxLength;
-        }
-        boolean passwordsMatch = password1Text.equals(password2Text);
-
-        return (lengthOk && passwordsMatch);
-    }
-
     @Override
     public void onStart() {
         super.onStart();
@@ -356,25 +329,53 @@ public class PasswordEntryDialog extends ThreemaDialogFragment implements Generi
             }
         }
 
+        updateViews();
+
+        ColorStateList colorStateList = DialogUtil.getButtonColorStateList(activity);
+
+        alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setTextColor(colorStateList);
+        alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setTextColor(colorStateList);
+    }
+
+    private void updateViews() {
+        var okButton = alertDialog.getButton(DialogInterface.BUTTON_POSITIVE);
         final TextInputEditText editText1 = alertDialog.findViewById(R.id.password1);
+        final TextInputLayout editText1Layout = alertDialog.findViewById(R.id.password1layout);
+        if (editText1Layout == null || editText1 == null || editText1.getText() == null) {
+            return;
+        }
+        var password1 = editText1.getText().toString();
 
-        if (isLengthCheck) {
+        if (requiresPasswordConfirmation) {
             final TextInputEditText editText2 = alertDialog.findViewById(R.id.password2);
+            final TextInputLayout editText2Layout = alertDialog.findViewById(R.id.password2layout);
+            if (editText2Layout == null || editText2 == null || editText2.getText() == null) {
+                return;
+            }
+            var password2 = editText2.getText().toString();
 
-            if (editText1 != null && editText2 != null) {
-                if (editText1.getText() != null && editText2.getText() != null) {
-                    alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(getPasswordOK(editText1.getText().toString(), editText2.getText().toString()));
-                }
+            if (password1.length() < minLength) {
+                editText1Layout.setError(null);
+                editText2Layout.setError(null);
+                okButton.setEnabled(false);
+                return;
             }
-        } else {
-            if (editText1 != null && editText1.getText() != null) {
-                alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(editText1.getText().length() > 0);
+            if (!password1.equals(password2)) {
+                editText1Layout.setError(null);
+                editText2Layout.setError(
+                    password2.length() >= password1.length()
+                        ? getString(R.string.passwords_dont_match)
+                        : null
+                );
+                okButton.setEnabled(false);
+                return;
             }
-        }
-
-        ColorStateList colorStateList = DialogUtil.getButtonColorStateList(activity);
 
-        alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setTextColor(colorStateList);
-        alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setTextColor(colorStateList);
+            editText1Layout.setError(null);
+            editText2Layout.setError(null);
+            okButton.setEnabled(true);
+        } else {
+            okButton.setEnabled(!password1.isEmpty());
+        }
     }
 }

+ 6 - 0
app/src/main/java/ch/threema/app/fragments/ComposeMessageFragment.java

@@ -1065,6 +1065,7 @@ public class ComposeMessageFragment extends Fragment implements
                 if (composeMessageAdapter != null) {
                     int index = composeMessageAdapter.getNextVoiceMessage(messageModel);
                     if (index != AbsListView.INVALID_POSITION) {
+                        logger.info("Playing next audio message at index {}", index);
                         View view = composeMessageAdapter.getView(index, null, null);
 
                         ComposeMessageHolder holder = (ComposeMessageHolder) view.getTag();
@@ -2495,6 +2496,11 @@ public class ComposeMessageFragment extends Fragment implements
             this.isGroupChat = true;
             if (this.groupDbId == 0) {
                 this.groupDbId = intent.getLongExtra(AppConstants.INTENT_DATA_GROUP_DATABASE_ID, 0L);
+
+                // TODO(ANDR-4303): This fallback is currently needed to handle intents coming from pinned shortcuts created before 6.2.0
+                if (groupDbId == 0L) {
+                    groupDbId = (long) intent.getIntExtra(AppConstants.INTENT_DATA_GROUP_DATABASE_ID, 0);
+                }
             }
             this.groupModel = groupModelRepository.getByLocalGroupDbId(this.groupDbId);
 

+ 3 - 0
app/src/main/java/ch/threema/app/fragments/ConversationsFragment.kt

@@ -131,6 +131,7 @@ import ch.threema.app.utils.IntentDataUtil
 import ch.threema.app.utils.LogUtil
 import ch.threema.app.utils.MimeUtil
 import ch.threema.app.utils.RuntimeUtil
+import ch.threema.app.utils.WidgetUtil
 import ch.threema.app.voip.activities.GroupCallActivity
 import ch.threema.app.voip.groupcall.GroupCallManager
 import ch.threema.base.utils.LoggingUtil
@@ -476,6 +477,7 @@ class ConversationsFragment :
                         changedPositions = null,
                         runAfterSetData = Thread { this.firePrivateReceiverUpdate() },
                     )
+                    context?.let(WidgetUtil::updateWidgets)
                 }
                 true
             }
@@ -561,6 +563,7 @@ class ConversationsFragment :
                     changedPositions = null,
                     runAfterSetData = Thread { firePrivateReceiverUpdate() },
                 )
+                context?.let(WidgetUtil::updateWidgets)
             }
 
             ID_RETURN_FROM_SECURITY_SETTINGS -> if (ConfigUtils.hasProtection(preferenceService)) {

+ 1 - 1
app/src/main/java/ch/threema/app/glide/IdentityAvatarFetcher.kt

@@ -196,7 +196,7 @@ class IdentityAvatarFetcher(
         highRes: Boolean,
     ): Bitmap? {
         if (ContactUtil.isGatewayContact(contactModel.identity) || AndroidContactUtil.getInstance()
-                .getAndroidContactUri(contactModel) == null
+                .getAndroidContactUri(contactModel, context) == null
         ) {
             return null
         }

+ 5 - 1
app/src/main/java/ch/threema/app/processors/incomingcspmessage/conversation/IncomingContactFileMessageTask.kt

@@ -74,7 +74,11 @@ class IncomingContactFileMessageTask(
         messageService.getContactMessageModel(
             message.messageId,
             message.fromIdentity,
-        )?.run { return ReceiveStepsResult.DISCARD }
+        )?.run {
+            // In this case the message has been processed earlier. Therefore we consider this as success. This causes the message to be reflected.
+            logger.info("Message model already exists. Aborting successfully.")
+            return ReceiveStepsResult.SUCCESS
+        }
 
         val fileData: FileData = message.fileData ?: run {
             logger.error("Discarding message ${message.messageId}: Missing file data")

+ 5 - 1
app/src/main/java/ch/threema/app/processors/incomingcspmessage/conversation/IncomingGroupFileMessageTask.kt

@@ -91,7 +91,11 @@ class IncomingGroupFileMessageTask(
             message.messageId,
             message.groupCreator,
             message.apiGroupId,
-        )?.run { return ReceiveStepsResult.DISCARD }
+        )?.run {
+            // In this case the message has been processed earlier. Therefore we consider this as success. This causes the message to be reflected.
+            logger.info("Message model already exists. Aborting successfully.")
+            return ReceiveStepsResult.SUCCESS
+        }
 
         val fileData: FileData = message.fileData ?: run {
             logger.error("Discarding message ${message.messageId}: Missing file data")

+ 2 - 2
app/src/main/java/ch/threema/app/routines/SynchronizeContactsRoutine.java

@@ -332,8 +332,8 @@ public class SynchronizeContactsRoutine implements Runnable {
                 try {
                     boolean createNewRawContact;
 
-                    AndroidContactUtil.getInstance().updateNameByAndroidContact(contact, androidContactSyncLogger); // throws an exception if no name can be determined
-                    AndroidContactUtil.getInstance().updateAvatarByAndroidContact(contact);
+                    AndroidContactUtil.getInstance().updateNameByAndroidContact(contact, androidContactSyncLogger, context); // throws an exception if no name can be determined
+                    AndroidContactUtil.getInstance().updateAvatarByAndroidContact(contact, context);
 
                     contact.setAcquaintanceLevelFromLocal(AcquaintanceLevel.DIRECT);
                     if (data.verificationLevel == VerificationLevel.UNVERIFIED) {

+ 4 - 3
app/src/main/java/ch/threema/app/services/ContactServiceImpl.java

@@ -123,6 +123,7 @@ public class ContactServiceImpl implements ContactService {
 
     private static final int TYPING_RECEIVE_TIMEOUT = (int) DateUtils.SECOND_IN_MILLIS * 15;
 
+    @NonNull
     private final Context context;
     private final AvatarCacheService avatarCacheService;
     private final DatabaseContactStore contactStore;
@@ -180,7 +181,7 @@ public class ContactServiceImpl implements ContactService {
     };
 
     public ContactServiceImpl(
-        Context context,
+        @NonNull Context context,
         DatabaseContactStore contactStore,
         AvatarCacheService avatarCacheService,
         DatabaseService databaseService,
@@ -903,7 +904,7 @@ public class ContactServiceImpl implements ContactService {
     @Override
     @WorkerThread
     public boolean updateAllContactNamesFromAndroidContacts() {
-        if (ContextCompat.checkSelfPermission(ThreemaApplication.getAppContext(), Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
+        if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
             return false;
         }
 
@@ -917,7 +918,7 @@ public class ContactServiceImpl implements ContactService {
             })
             .forEach(contactModel -> {
                 try {
-                    AndroidContactUtil.getInstance().updateNameByAndroidContact(contactModel, androidContactSyncLogger);
+                    AndroidContactUtil.getInstance().updateNameByAndroidContact(contactModel, androidContactSyncLogger, context);
                 } catch (ThreemaException e) {
                     logger.error("Unable to update contact name", e);
                 }

+ 5 - 4
app/src/main/java/ch/threema/app/services/FileServiceImpl.java

@@ -131,6 +131,7 @@ public class FileServiceImpl implements FileService {
 
     private static final String DIALOG_TAG_SAVING_MEDIA = "savingToGallery";
 
+    @NonNull
     private final Context context;
     @NonNull
     private final AppDirectoryProvider appDirectoryProvider;
@@ -148,14 +149,14 @@ public class FileServiceImpl implements FileService {
     private final AvatarCacheService avatarCacheService;
 
     public FileServiceImpl(
-        @NonNull Context c,
+        @NonNull Context context,
         @NonNull AppDirectoryProvider appDirectoryProvider,
         @NonNull MasterKeyProvider masterKeyProvider,
         @NonNull PreferenceService preferenceService,
         @NonNull NotificationPreferenceService notificationPreferenceService,
         @NonNull AvatarCacheService avatarCacheService
     ) {
-        this.context = c;
+        this.context = context;
         this.appDirectoryProvider = appDirectoryProvider;
         this.preferenceService = preferenceService;
         this.masterKeyProvider = masterKeyProvider;
@@ -1085,7 +1086,7 @@ public class FileServiceImpl implements FileService {
 
     @Override
     @Nullable
-    public Bitmap getAndroidDefinedProfilePicture(@NonNull ContactModel contactModel) throws Exception {
+    public Bitmap getAndroidDefinedProfilePicture(@NonNull ContactModel contactModel) {
         ContactModelData contactModelData = contactModel.getData();
         if (contactModelData == null) {
             logger.error("Contact model data is null");
@@ -1098,7 +1099,7 @@ public class FileServiceImpl implements FileService {
             ServiceManager serviceManager = ThreemaApplication.getServiceManager();
             if (serviceManager != null) {
                 try {
-                    AndroidContactUtil.getInstance().updateAvatarByAndroidContact(contactModel);
+                    AndroidContactUtil.getInstance().updateAvatarByAndroidContact(contactModel, context);
                 } catch (SecurityException e) {
                     logger.error("Could not update avatar by android contact", e);
                 }

+ 15 - 15
app/src/main/java/ch/threema/app/services/PinLockService.java

@@ -114,26 +114,26 @@ public class PinLockService implements LockAppService {
         return true;
     }
 
-    private void updateState(boolean locked) {
-        logger.info("update locked stated to: {} ", isLocked());
-        if (this.locked != locked) {
-
-            this.locked = locked;
+    private void updateState(boolean isLocked) {
+        if (this.locked == isLocked) {
+            return;
+        }
+        logger.info("updating locked stated from {} to {} ", this.locked, isLocked);
+        this.locked = isLocked;
 
-            synchronized (this.lockAppStateChangedItems) {
-                ArrayList<OnLockAppStateChanged> toRemove = new ArrayList<>();
+        synchronized (this.lockAppStateChangedItems) {
+            ArrayList<OnLockAppStateChanged> toRemove = new ArrayList<>();
 
-                for (OnLockAppStateChanged c : this.lockAppStateChangedItems) {
-                    if (c.changed(locked)) {
-                        toRemove.add(c);
-                    }
+            for (OnLockAppStateChanged c : this.lockAppStateChangedItems) {
+                if (c.changed(isLocked)) {
+                    toRemove.add(c);
                 }
-                this.lockAppStateChangedItems.removeAll(toRemove);
             }
-
-            // update widget
-            WidgetUtil.updateWidgets(context);
+            this.lockAppStateChangedItems.removeAll(toRemove);
         }
+
+        // update widget
+        WidgetUtil.updateWidgets(context);
     }
 
     @Override

+ 0 - 100
app/src/main/java/ch/threema/app/services/WidgetService.java

@@ -1,100 +0,0 @@
-/*  _____ _
- * |_   _| |_  _ _ ___ ___ _ __  __ _
- *   | | | ' \| '_/ -_) -_) '  \/ _` |_
- *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
- *
- * Threema for Android
- * Copyright (c) 2015-2025 Threema GmbH
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package ch.threema.app.services;
-
-import android.content.Intent;
-import android.widget.RemoteViews;
-import android.widget.RemoteViewsService;
-
-import org.slf4j.Logger;
-
-import androidx.annotation.NonNull;
-import ch.threema.app.adapters.WidgetViewsFactory;
-import ch.threema.base.ThreemaException;
-import ch.threema.base.utils.LoggingUtil;
-
-public class WidgetService extends RemoteViewsService {
-    private static final Logger logger = LoggingUtil.getThreemaLogger("WidgetService");
-
-    @Override
-    public RemoteViewsFactory onGetViewFactory(Intent intent) {
-        logger.debug("onGetViewFactory");
-        try {
-            return new WidgetViewsFactory(this.getApplicationContext());
-        } catch (ThreemaException e) {
-            logger.error("Could not create widget views factory");
-            // We use an empty views factory as we cannot do anything if the actual views factory
-            // cannot be created.
-            return getEmptyViewsFactory();
-        }
-    }
-
-    @NonNull
-    private RemoteViewsFactory getEmptyViewsFactory() {
-        return new RemoteViewsFactory() {
-            @Override
-            public void onCreate() {
-                // Nothing to do
-            }
-
-            @Override
-            public void onDataSetChanged() {
-                // Nothing to do
-            }
-
-            @Override
-            public void onDestroy() {
-                // Nothing to do
-            }
-
-            @Override
-            public int getCount() {
-                return 0;
-            }
-
-            @Override
-            public RemoteViews getViewAt(int i) {
-                return null;
-            }
-
-            @Override
-            public RemoteViews getLoadingView() {
-                return null;
-            }
-
-            @Override
-            public int getViewTypeCount() {
-                return 0;
-            }
-
-            @Override
-            public long getItemId(int i) {
-                return 0;
-            }
-
-            @Override
-            public boolean hasStableIds() {
-                return false;
-            }
-        };
-    }
-}

+ 31 - 0
app/src/main/java/ch/threema/app/services/WidgetService.kt

@@ -0,0 +1,31 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2015-2025 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.app.services
+
+import android.content.Intent
+import android.widget.RemoteViewsService
+import ch.threema.app.adapters.WidgetViewsFactory
+
+class WidgetService : RemoteViewsService() {
+    override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory =
+        WidgetViewsFactory(applicationContext)
+}

+ 5 - 6
app/src/main/java/ch/threema/app/services/license/LicenseServiceSerial.java

@@ -23,7 +23,6 @@ package ch.threema.app.services.license;
 
 import androidx.annotation.Nullable;
 import ch.threema.app.preference.service.PreferenceService;
-import ch.threema.app.utils.TestUtil;
 import ch.threema.domain.models.SerialCredentials;
 import ch.threema.domain.protocol.api.APIConnector;
 
@@ -35,7 +34,8 @@ public class LicenseServiceSerial extends LicenseServiceThreema<SerialCredential
 
     @Override
     public boolean hasCredentials() {
-        return !TestUtil.isEmptyOrNull(this.preferenceService.getSerialNumber());
+        var serialNumber = preferenceService.getSerialNumber();
+        return serialNumber != null && !serialNumber.isEmpty();
     }
 
     @Override
@@ -51,10 +51,9 @@ public class LicenseServiceSerial extends LicenseServiceThreema<SerialCredential
     @Override
     @Nullable
     public SerialCredentials loadCredentials() {
-        String licenseKey = this.preferenceService.getSerialNumber();
-
-        if (!TestUtil.isEmptyOrNull(licenseKey)) {
-            return new SerialCredentials(licenseKey);
+        var serialNumber = preferenceService.getSerialNumber();
+        if (serialNumber != null && !serialNumber.isEmpty()) {
+            return new SerialCredentials(serialNumber);
         }
         return null;
     }

+ 0 - 8
app/src/main/java/ch/threema/app/services/license/LicenseServiceThreema.java

@@ -27,7 +27,6 @@ import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 import ch.threema.app.preference.service.PreferenceService;
-import ch.threema.app.utils.TestUtil;
 import ch.threema.base.utils.LoggingUtil;
 import ch.threema.domain.models.LicenseCredentials;
 import ch.threema.domain.onprem.UnauthorizedFetchException;
@@ -51,13 +50,6 @@ abstract public class LicenseServiceThreema<T extends LicenseCredentials> implem
         this.isLicensed = preferenceService.getLicensedStatus();
     }
 
-    @Override
-    public boolean hasCredentials() {
-        return
-            !TestUtil.isEmptyOrNull(this.preferenceService.getSerialNumber())
-                || !TestUtil.isEmptyOrNull(this.preferenceService.getLicenseUsername(), this.preferenceService.getLicensePassword());
-    }
-
     @Override
     @Nullable
     @WorkerThread

+ 4 - 4
app/src/main/java/ch/threema/app/services/license/LicenseServiceUser.java

@@ -24,7 +24,6 @@ package ch.threema.app.services.license;
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 import ch.threema.app.preference.service.PreferenceService;
-import ch.threema.app.utils.TestUtil;
 import ch.threema.domain.models.UserCredentials;
 import ch.threema.domain.protocol.api.APIConnector;
 
@@ -36,8 +35,9 @@ public class LicenseServiceUser extends LicenseServiceThreema<UserCredentials> {
 
     @Override
     public boolean hasCredentials() {
-        return !TestUtil.isEmptyOrNull(this.preferenceService.getLicenseUsername(),
-            this.preferenceService.getLicensePassword());
+        var username = preferenceService.getLicenseUsername();
+        var password = preferenceService.getLicensePassword();
+        return username != null && !username.isEmpty() && password != null && !password.isEmpty();
     }
 
     @Override
@@ -58,7 +58,7 @@ public class LicenseServiceUser extends LicenseServiceThreema<UserCredentials> {
         String username = this.preferenceService.getLicenseUsername();
         String password = this.preferenceService.getLicensePassword();
 
-        if (!TestUtil.isEmptyOrNull(username, password)) {
+        if (username != null && !username.isEmpty() && password != null && !password.isEmpty()) {
             return new UserCredentials(username, password);
         }
         return null;

+ 1 - 1
app/src/main/java/ch/threema/app/services/messageplayer/AudioMessagePlayer.java

@@ -129,7 +129,7 @@ public class AudioMessagePlayer extends MessagePlayer {
 
         @Override
         public void onIsLoadingChanged(boolean isLoading) {
-            logger.info(isLoading ? "onLoading" : "onLoaded");
+            logger.info(isLoading ? "is now loading" : "is not loading");
         }
 
         @Override

+ 7 - 13
app/src/main/java/ch/threema/app/stores/EncryptedPreferenceStoreImpl.kt

@@ -24,15 +24,15 @@ package ch.threema.app.stores
 import android.content.Context
 import ch.threema.app.listeners.PreferenceListener
 import ch.threema.app.managers.ListenerManager
-import ch.threema.app.utils.FileUtil
 import ch.threema.app.utils.StringConversionUtil
 import ch.threema.base.utils.LoggingUtil
+import ch.threema.base.writeAtomically
 import ch.threema.common.takeUnlessEmpty
 import ch.threema.localcrypto.MasterKey
 import java.io.File
 import java.io.FileInputStream
-import java.io.FileOutputStream
 import java.io.IOException
+import kotlin.text.toByteArray
 import org.apache.commons.io.IOUtils
 import org.json.JSONArray
 import org.json.JSONObject
@@ -166,18 +166,12 @@ class EncryptedPreferenceStoreImpl(
 
     private fun saveDataToEncryptedFile(data: ByteArray, key: String) {
         val file = getEncryptedFile(key)
-        if (!file.exists()) {
-            try {
-                FileUtil.createNewFileOrLog(file, logger)
-            } catch (e: Exception) {
-                // TODO(ANDR-4060): Throw a proper exception here
-                logger.error("Failed to create encrypted file with key {}", key, e)
-            }
-        }
         try {
-            FileOutputStream(file).use { fileOutputStream ->
-                masterKey.getCipherOutputStream(fileOutputStream).use { cipherOutputStream ->
-                    cipherOutputStream.write(data)
+            synchronized(this) {
+                file.writeAtomically { fileOutputStream ->
+                    masterKey.getCipherOutputStream(fileOutputStream).use { cipherOutputStream ->
+                        cipherOutputStream.write(data)
+                    }
                 }
             }
         } catch (e: IOException) {

+ 3 - 0
app/src/main/java/ch/threema/app/systemupdates/updates/SystemUpdateToVersion113.kt

@@ -28,6 +28,9 @@ import ch.threema.data.models.GroupModel
 class SystemUpdateToVersion113(private val serviceManager: ServiceManager) : SystemUpdate {
 
     override fun run() {
+        if (!serviceManager.userService.hasIdentity()) {
+            return
+        }
         serviceManager.modelRepositories.groups.getAll()
             .filter(GroupModel::isCreator)
             .forEach(::scheduleGroupUpdate)

+ 5 - 1
app/src/main/java/ch/threema/app/ui/DirectoryDataSourceFactory.java

@@ -21,21 +21,25 @@
 
 package ch.threema.app.ui;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.lifecycle.MutableLiveData;
 import androidx.paging.DataSource;
 import ch.threema.domain.protocol.api.work.WorkDirectory;
 import ch.threema.domain.protocol.api.work.WorkDirectoryContact;
 
 public class DirectoryDataSourceFactory extends DataSource.Factory<WorkDirectory, WorkDirectoryContact> {
-    private boolean init = false;
+    private boolean init;
 
     // Used to hold a reference to the data source
+    @Nullable
     public MutableLiveData<DirectoryDataSource> postLiveData;
 
     public DirectoryDataSourceFactory() {
         this.init = true;
     }
 
+    @NonNull
     @Override
     public DataSource<WorkDirectory, WorkDirectoryContact> create() {
         DirectoryDataSource dataSource = new DirectoryDataSource();

+ 7 - 5
app/src/main/java/ch/threema/app/usecases/conversations/WatchConversationListItemsUseCase.kt

@@ -297,11 +297,13 @@ abstract class WatchConversationListItemsUseCase(
         return if (latestMessage.isOutbox) {
             ResourceIdString(R.string.me_myself_and_i)
         } else {
-            ResolvedString(
-                NameUtil.getShortName(
-                    contactService.getByIdentity(latestMessage.identity),
-                ),
-            )
+            NameUtil.getShortName(
+                contactService.getByIdentity(latestMessage.identity),
+            )?.let { shortName ->
+                ResolvedString(
+                    string = shortName,
+                )
+            }
         }
     }
 }

+ 57 - 22
app/src/main/java/ch/threema/app/utils/AndroidContactUtil.java

@@ -38,7 +38,6 @@ import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
-import android.os.Build;
 import android.provider.ContactsContract;
 import android.widget.Toast;
 
@@ -63,6 +62,7 @@ import java.util.TreeMap;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.debug.AndroidContactSyncLogger;
+import ch.threema.app.debug.LookupKeyAndContactId;
 import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.services.FileService;
 import ch.threema.app.services.UserService;
@@ -153,9 +153,8 @@ public class AndroidContactUtil {
      * @return a valid uri pointing to the android contact or null if permission was not granted, no android contact is linked or android contact could not be looked up
      */
     @Nullable
-    public Uri getAndroidContactUri(@NonNull ContactModel contactModel) {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
-            ContextCompat.checkSelfPermission(ThreemaApplication.getAppContext(), Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
+    public Uri getAndroidContactUri(@NonNull ContactModel contactModel, @NonNull Context context) {
+        if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
             return null;
         }
 
@@ -175,11 +174,9 @@ public class AndroidContactUtil {
      * If there's no avatar for this Android contact, any current avatar file will be deleted.
      * <p>
      * It is safe to call this method even if permission to read contacts is not given
-     *
-     * @param contactModel ContactModel
      */
     @RequiresPermission(Manifest.permission.READ_CONTACTS)
-    public void updateAvatarByAndroidContact(@NonNull ContactModel contactModel) {
+    public void updateAvatarByAndroidContact(@NonNull ContactModel contactModel, @NonNull Context context) {
         if (fileService == null) {
             logger.info("FileService not available");
             return;
@@ -198,7 +195,7 @@ public class AndroidContactUtil {
         }
 
         // contactUri will be null if permission is not granted
-        Uri contactUri = getAndroidContactUri(contactModel);
+        Uri contactUri = getAndroidContactUri(contactModel, context);
         if (contactUri != null) {
             Bitmap bitmap = AvatarConverterUtil.convert(ThreemaApplication.getAppContext(), contactUri);
 
@@ -231,20 +228,20 @@ public class AndroidContactUtil {
     /**
      * Update the name of this contact according to the name of the Android contact
      *
-     * @param contactModel ContactModel
      * @param androidContactSyncLogger this logger can be provided to log duplicate contact names
      */
     @RequiresPermission(Manifest.permission.READ_CONTACTS)
     public void updateNameByAndroidContact(
         @NonNull ContactModel contactModel,
-        @Nullable AndroidContactSyncLogger androidContactSyncLogger
+        @Nullable AndroidContactSyncLogger androidContactSyncLogger,
+        @NonNull Context context
     ) throws ThreemaException {
         ContactModelData data = contactModel.getData();
         if (data == null) {
             logger.warn("Contact model data is null");
             return;
         }
-        Uri namedContactUri = getAndroidContactUri(contactModel);
+        Uri namedContactUri = getAndroidContactUri(contactModel, context);
         if (namedContactUri == null) {
             logger.info("Unable to get android contact uri for {} obfuscatedLookupKey = {}", contactModel.getIdentity(), getObfuscatedAndroidContactLookupKey(data));
             return;
@@ -259,8 +256,9 @@ public class AndroidContactUtil {
             throw new ThreemaException("Unable to get contact name");
         }
 
-        if (androidContactSyncLogger != null) {
-            androidContactSyncLogger.addSyncedNames(namedContactUri, contactName.firstName, contactName.lastName);
+        if (androidContactSyncLogger != null && data.androidContactLookupKey != null) {
+            LookupKeyAndContactId lookupKeyAndContactId = getLookupKeyAndContactId(data.androidContactLookupKey);
+            androidContactSyncLogger.addSyncedNames(lookupKeyAndContactId, contactName.firstName, contactName.lastName);
         }
 
         if (!data.firstName.equals(contactName.firstName) || !data.lastName.equals(contactName.lastName)) {
@@ -281,7 +279,7 @@ public class AndroidContactUtil {
      */
     @RequiresPermission(Manifest.permission.READ_CONTACTS)
     @Nullable
-    private ContactName getContactName(Uri contactUri) {
+    private ContactName getContactName(@NonNull Uri contactUri) {
         if (this.contentResolver == null) {
             logger.error("Cannot get contact name as content resolver is null");
             return null;
@@ -301,6 +299,8 @@ public class AndroidContactUtil {
                     logger.error("Cannot get contact name as contact lookup key is null");
                     return null;
                 }
+                logLookupKeyDifference(contactUri, lookupKey);
+
                 contactName = this.getContactNameFromLookupKey(lookupKey);
 
                 // fallback
@@ -748,8 +748,8 @@ public class AndroidContactUtil {
      * @param contactModel Threema contact
      * @return true if the contact is linked with a system contact (even if no app is available for an ACTION_EDIT intent in the system), false otherwise
      */
-    public boolean openContactEditor(Context context, ContactModel contactModel, int requestCode) {
-        Uri contactUri = AndroidContactUtil.getInstance().getAndroidContactUri(contactModel);
+    public boolean openContactEditor(@NonNull Context context, @NonNull ContactModel contactModel, int requestCode) {
+        Uri contactUri = AndroidContactUtil.getInstance().getAndroidContactUri(contactModel, context);
 
         if (contactUri != null) {
             Intent intent = new Intent(Intent.ACTION_EDIT);
@@ -785,16 +785,51 @@ public class AndroidContactUtil {
         if (androidContactLookupKey == null) {
             return null;
         }
+
+        LookupKeyAndContactId lookupKeyAndContactId = getLookupKeyAndContactId(androidContactLookupKey);
+        int obfuscatedLookupKey = lookupKeyAndContactId.getLookupKey().hashCode();
+        long contactId = lookupKeyAndContactId.getContactId();
+        // Concatenate the obfuscated lookup key with the raw contact id
+        return obfuscatedLookupKey + "/"  + contactId;
+    }
+
+    @NonNull
+    private static LookupKeyAndContactId getLookupKeyAndContactId(@NonNull String androidContactLookupKey) {
         // The lookup key and contact id are separated by a slash
         int splitIndex = androidContactLookupKey.lastIndexOf('/');
         if (splitIndex == -1) {
             logger.warn("Unexpected lookup key format as it does not contain '/'");
-            return "unexpected-" + androidContactLookupKey.hashCode();
+            return new LookupKeyAndContactId(androidContactLookupKey, -1L);
+        }
+
+        String lookupKey = androidContactLookupKey.substring(0, splitIndex);
+        try {
+            long contactId = Long.parseLong(androidContactLookupKey.substring(splitIndex + 1));
+            return new LookupKeyAndContactId(lookupKey, contactId);
+        } catch (NumberFormatException e) {
+            logger.warn("Could not parse the contact id", e);
+            return new LookupKeyAndContactId(lookupKey, -2L);
+        }
+    }
+
+    private void logLookupKeyDifference(@NonNull Uri contactUri, @NonNull String lookupKey) {
+        String path = contactUri.getPath();
+        if (path == null) {
+            logger.warn("No path of contact uri");
+            return;
+        }
+
+        String lookupKeyAndContactIdFromUri = path.replace("/contacts/lookup/", "");
+        int lookupKeySeparationIndex = lookupKeyAndContactIdFromUri.lastIndexOf("/");
+        if (lookupKeySeparationIndex < 0) {
+            logger.warn("No / in lookup key and contact id from uri");
+            return;
+        }
+        String lookupKeyFromUri = lookupKeyAndContactIdFromUri.substring(0, lookupKeySeparationIndex);
+        if (lookupKey.equals(lookupKeyFromUri)) {
+            logger.info("Lookup key of uri matches the queried lookup key");
+        } else {
+            logger.warn("Queried lookup key is different ({} characters vs {} characters)", lookupKey.length(), lookupKeyFromUri.length());
         }
-        // Obfuscate the lookup key as it may contain sensitive data
-        int obfuscatedLookupKey = androidContactLookupKey.substring(0, splitIndex).hashCode();
-        String contactId = androidContactLookupKey.substring(splitIndex);
-        // Concatenate the obfuscated lookup key with the raw contact id
-        return obfuscatedLookupKey + contactId;
     }
 }

+ 5 - 0
app/src/main/java/ch/threema/app/utils/IntentDataUtil.java

@@ -390,6 +390,11 @@ public class IntentDataUtil {
             return contactService.createReceiver(contactService.getByIdentity(cIdentity));
         } else if (extras.containsKey(AppConstants.INTENT_DATA_GROUP_DATABASE_ID)) {
             int groupId = (int) extras.getLong(AppConstants.INTENT_DATA_GROUP_DATABASE_ID, 0L);
+
+            // TODO(ANDR-4303): This fallback is currently needed to handle intents coming from pinned shortcuts created before 6.2.0
+            if (groupId == 0) {
+                groupId = extras.getInt(AppConstants.INTENT_DATA_GROUP_DATABASE_ID, 0);
+            }
             final @Nullable GroupModel groupModel = groupService.getById(groupId);
             if (groupModel != null) {
                 return groupService.createReceiver(groupModel);

+ 1 - 0
app/src/main/java/ch/threema/app/utils/NameUtil.java

@@ -179,6 +179,7 @@ public class NameUtil {
         return shortname != null ? shortname : identity;
     }
 
+    @Nullable
     public static String getShortName(ContactModel model) {
         if (model != null) {
             if (TestUtil.isEmptyOrNull(model.getFirstName())) {

+ 5 - 18
app/src/main/java/ch/threema/app/utils/ShortcutUtil.java

@@ -323,13 +323,11 @@ public final class ShortcutUtil {
      * You may want to call deleteAllShareTargetShortcuts() before adding new dynamic shortcuts as they are limited
      */
     @WorkerThread
-    public static void publishRecentChatsAsShareTargets() {
-        if (ThreemaApplication.getServiceManager() == null) {
-            return;
-        }
-
-        PreferenceService preferenceService = ThreemaApplication.getServiceManager().getPreferenceService();
-        if (preferenceService == null || !preferenceService.isDirectShare()) {
+    public static void publishRecentChatsAsShareTargets(
+        @NonNull PreferenceService preferenceService,
+        @NonNull ConversationService conversationService
+    ) {
+        if (!preferenceService.isDirectShare()) {
             return;
         }
 
@@ -338,17 +336,6 @@ public final class ShortcutUtil {
             return;
         }
 
-        ConversationService conversationService = null;
-        try {
-            conversationService = ThreemaApplication.getServiceManager().getConversationService();
-        } catch (ThreemaException e) {
-            return;
-        }
-
-        if (conversationService == null) {
-            return;
-        }
-
         if (BackupService.isRunning() || RestoreService.isRunning()) {
             logger.info("Backup / Restore is running. Exiting");
             return;

+ 5 - 2
app/src/main/java/ch/threema/app/utils/TestUtil.java

@@ -59,9 +59,12 @@ public class TestUtil {
     }
 
     /**
-     * Returns true if the provided strings are null or empty. Returns false if at least one
-     * of them is not empty.
+     * Returns true if the provided strings are null or empty. Returns false if at least one of them is not empty.
+     * <p>
+     * Do not use this, as it is not intuitively clear whether it performs a logical AND or an OR on the inputs' nullability and emptiness,
+     * which can easily lead to subtle bugs.
      */
+    @Deprecated
     public static boolean isEmptyOrNull(@Nullable String... string) {
         if (string == null) {
             return true;

+ 0 - 47
app/src/main/java/ch/threema/app/utils/WidgetUtil.java

@@ -1,47 +0,0 @@
-/*  _____ _
- * |_   _| |_  _ _ ___ ___ _ __  __ _
- *   | | | ' \| '_/ -_) -_) '  \/ _` |_
- *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
- *
- * Threema for Android
- * Copyright (c) 2015-2025 Threema GmbH
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package ch.threema.app.utils;
-
-import android.appwidget.AppWidgetManager;
-import android.content.ComponentName;
-import android.content.Context;
-
-import org.slf4j.Logger;
-
-import ch.threema.app.R;
-import ch.threema.app.receivers.WidgetProvider;
-import ch.threema.base.utils.LoggingUtil;
-
-public class WidgetUtil {
-    private static final Logger logger = LoggingUtil.getThreemaLogger("WidgetUtil");
-
-    public static void updateWidgets(Context context) {
-        logger.debug("Update Widgets");
-
-        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
-        final int[] widgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, WidgetProvider.class));
-
-        for (int widgetId : widgetIds) {
-            appWidgetManager.notifyAppWidgetViewDataChanged(widgetId, R.id.widget_list);
-        }
-    }
-}

+ 37 - 0
app/src/main/java/ch/threema/app/utils/WidgetUtil.kt

@@ -0,0 +1,37 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2015-2025 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.app.utils
+
+import android.appwidget.AppWidgetManager
+import android.content.ComponentName
+import android.content.Context
+import ch.threema.app.R
+import ch.threema.app.receivers.WidgetProvider
+
+object WidgetUtil {
+    @JvmStatic
+    fun updateWidgets(context: Context) {
+        val appWidgetManager = AppWidgetManager.getInstance(context)
+        val widgetIds = appWidgetManager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java))
+        appWidgetManager.notifyAppWidgetViewDataChanged(widgetIds, R.id.widget_list)
+    }
+}

+ 17 - 18
app/src/main/java/ch/threema/app/workers/ShareTargetUpdateWorker.kt

@@ -37,12 +37,14 @@ import androidx.work.WorkerParameters
 import ch.threema.app.BuildConfig
 import ch.threema.app.backuprestore.csv.BackupService
 import ch.threema.app.di.awaitServiceManagerWithTimeout
+import ch.threema.app.preference.service.PreferenceService
+import ch.threema.app.services.ConversationService
 import ch.threema.app.utils.ShortcutUtil
-import ch.threema.app.utils.WorkManagerUtil
 import ch.threema.base.utils.LoggingUtil
 import java.util.concurrent.TimeUnit
 import kotlin.time.Duration.Companion.seconds
 import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
 
 private val logger = LoggingUtil.getThreemaLogger("ShareTargetUpdateWorker")
 
@@ -50,6 +52,10 @@ class ShareTargetUpdateWorker(
     context: Context,
     workerParams: WorkerParameters,
 ) : CoroutineWorker(context, workerParams), KoinComponent {
+
+    private val preferenceService: PreferenceService by inject()
+    private val conversationService: ConversationService by inject()
+
     override suspend fun doWork(): Result {
         logger.info("Updating share target shortcuts")
 
@@ -57,7 +63,7 @@ class ShareTargetUpdateWorker(
             ?: return Result.failure()
 
         if (serviceManager.userService.hasIdentity() && !BackupService.isRunning()) {
-            ShortcutUtil.publishRecentChatsAsShareTargets()
+            ShortcutUtil.publishRecentChatsAsShareTargets(preferenceService, conversationService)
         }
 
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@@ -85,24 +91,17 @@ class ShareTargetUpdateWorker(
             try {
                 val workManager = WorkManager.getInstance(context)
 
-                if (WorkManagerUtil.shouldScheduleNewWorkManagerInstance(workManager, WORKER_SHARE_TARGET_UPDATE, schedulePeriod)) {
-                    logger.debug("Create new worker")
-
-                    // schedule the start of the service according to schedule period
-                    val constraints = Constraints.Builder()
-                        .setRequiredNetworkType(NetworkType.CONNECTED)
-                        .build()
+                // schedule the start of the service according to schedule period
+                val constraints = Constraints.Builder()
+                    .setRequiredNetworkType(NetworkType.CONNECTED)
+                    .build()
 
-                    val workRequest = PeriodicWorkRequest.Builder(ShareTargetUpdateWorker::class.java, schedulePeriod, TimeUnit.MILLISECONDS)
-                        .setConstraints(constraints)
-                        .addTag(schedulePeriod.toString())
-                        .setInitialDelay(3, TimeUnit.MINUTES)
-                        .build()
+                val workRequest = PeriodicWorkRequest.Builder(ShareTargetUpdateWorker::class.java, schedulePeriod, TimeUnit.MILLISECONDS)
+                    .setConstraints(constraints)
+                    .setInitialDelay(20, TimeUnit.SECONDS) // give the app some time to update conversations first when the app is started
+                    .build()
 
-                    workManager.enqueueUniquePeriodicWork(WORKER_SHARE_TARGET_UPDATE, ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, workRequest)
-                } else {
-                    logger.debug("Reusing existing worker")
-                }
+                workManager.enqueueUniquePeriodicWork(WORKER_SHARE_TARGET_UPDATE, ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, workRequest)
             } catch (e: IllegalStateException) {
                 logger.error("Unable to schedule share target update work", e)
                 return false

+ 53 - 0
app/src/main/java/ch/threema/base/FileExtensions.kt

@@ -0,0 +1,53 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2024-2025 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.base
+
+import androidx.core.util.AtomicFile
+import ch.threema.annotation.SameThread
+import ch.threema.common.NoCloseOutputStream
+import java.io.DataOutputStream
+import java.io.File
+import java.io.IOException
+import java.io.OutputStream
+
+/**
+ * Writes a file atomically, i.e., it is either fully written or not at all.
+ * If the file or its parent directory did not previously exist, it will be created.
+ * If the file did previously exist, it will be replaced.
+ *
+ * The output stream passed into [performWrite] does not need to be closed.
+ */
+@SameThread
+fun File.writeAtomically(performWrite: (OutputStream) -> Unit) {
+    val atomicKeyFile = AtomicFile(this)
+    val fos = atomicKeyFile.startWrite()
+    try {
+        // Note: stream *must not* be closed explicitly (see AtomicFile documentation)
+        val dos = NoCloseOutputStream(DataOutputStream(fos))
+        performWrite(dos)
+        dos.flush()
+    } catch (e: IOException) {
+        atomicKeyFile.failWrite(fos)
+        throw e
+    }
+    atomicKeyFile.finishWrite(fos)
+}

+ 1 - 0
app/src/main/java/ch/threema/localcrypto/MasterKeyImpl.kt

@@ -87,6 +87,7 @@ class MasterKeyImpl(
 
             return cipherOutputStream
         } finally {
+            // TODO(ANDR-4305): The stream should not be closed here. Check if this can be removed.
             if (cipherOutputStream == null) {
                 // close the output stream here as long as it's not attached to a CipherOutputStream
                 outputStream.close()

+ 11 - 0
app/src/main/java/ch/threema/localcrypto/MasterKeyStorageManager.kt

@@ -21,8 +21,10 @@
 
 package ch.threema.localcrypto
 
+import ch.threema.common.secureContentEquals
 import ch.threema.localcrypto.models.MasterKeyState
 import ch.threema.localcrypto.models.MasterKeyStorageData
+import ch.threema.localcrypto.models.Version1MasterKeyStorageData
 import java.io.IOException
 import kotlin.Throws
 
@@ -49,6 +51,15 @@ class MasterKeyStorageManager(
 
     @Throws(IOException::class)
     fun writeKey(data: MasterKeyState) {
+        if (version1KeyFileManager.keyFileExists() && data is MasterKeyState.Plain) {
+            val oldKeyData = version1KeyFileManager.readKeyFile().data
+            if (oldKeyData is Version1MasterKeyStorageData.Unprotected) {
+                if (!oldKeyData.masterKeyData.value.secureContentEquals(data.masterKeyData.value)) {
+                    error("Migrated master key differs from original master key")
+                }
+            }
+        }
+
         version2KeyFileManager.writeKeyFile(storageStateConverter.toStorageData(data))
         version1KeyFileManager.deleteFile()
     }

+ 1 - 1
app/src/main/java/ch/threema/localcrypto/Version1MasterKeyFileManager.kt

@@ -53,7 +53,7 @@ class Version1MasterKeyFileManager(
     fun keyFileExists() = keyFile.exists()
 
     @Throws(IOException::class)
-    fun readKeyFile(): MasterKeyStorageData {
+    fun readKeyFile(): MasterKeyStorageData.Version1 {
         val atomicKeyFile = AtomicFile(keyFile)
 
         DataInputStream(atomicKeyFile.openRead()).use { dis ->

+ 2 - 18
app/src/main/java/ch/threema/localcrypto/Version2MasterKeyFileManager.kt

@@ -22,12 +22,11 @@
 package ch.threema.localcrypto
 
 import androidx.core.util.AtomicFile
+import ch.threema.base.writeAtomically
 import ch.threema.localcrypto.models.MasterKeyStorageData
 import java.io.DataInputStream
-import java.io.DataOutputStream
 import java.io.File
 import java.io.IOException
-import java.io.OutputStream
 
 /**
  * Handles reading from and writing to the version 2 master key file,
@@ -51,23 +50,8 @@ class Version2MasterKeyFileManager(
 
     @Throws(IOException::class)
     fun writeKeyFile(masterKeyStorageData: MasterKeyStorageData.Version2) {
-        writeKeyFile { outputStream ->
+        keyFile.writeAtomically { outputStream ->
             encoder.encodeMasterKeyStorageData(masterKeyStorageData).writeTo(outputStream)
         }
     }
-
-    private fun writeKeyFile(performWrite: (OutputStream) -> Unit) {
-        val atomicKeyFile = AtomicFile(keyFile)
-        val fos = atomicKeyFile.startWrite()
-        try {
-            // Note: stream *must not* be closed explicitly (see AtomicFile documentation)
-            val dos = DataOutputStream(fos)
-            performWrite(dos)
-            dos.flush()
-        } catch (e: IOException) {
-            atomicKeyFile.failWrite(fos)
-            throw e
-        }
-        atomicKeyFile.finishWrite(fos)
-    }
 }

+ 4 - 1
app/src/main/res/layout/activity_unlock_masterkey.xml

@@ -42,7 +42,7 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_below="@id/layout_top"
-            android:layout_marginTop="@dimen/grid_unit_x2"
+            android:layout_marginTop="@dimen/grid_unit_x1_5"
             android:gravity="center_vertical"
             android:orientation="horizontal">
 
@@ -64,6 +64,8 @@
                 android:layout_marginTop="5dp"
                 android:layout_weight="2"
                 android:hint="@string/masterkey_passphrase_hint"
+                android:paddingBottom="@dimen/grid_unit_x0_5"
+                android:paddingTop="@dimen/grid_unit_x0_5"
                 app:counterEnabled="false"
                 app:errorEnabled="true"
                 app:passwordToggleEnabled="true">
@@ -102,6 +104,7 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_below="@id/unlock_layout"
+            android:layout_marginTop="@dimen/grid_unit_x1"
             android:layout_marginBottom="5dp"
             android:drawablePadding="10dp"
             android:text="@string/masterkey_lock_explain"

+ 1 - 0
app/src/main/res/layout/dialog_password_entry.xml

@@ -78,6 +78,7 @@
             android:id="@+id/check_box"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:layout_marginTop="@dimen/grid_unit_x0_5"
             android:textAppearance="@style/Threema.TextAppearance.Dialog.Body"
             android:visibility="gone"
             tools:text="Check box"

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

@@ -545,6 +545,7 @@
     <string name="invalid_backup">Няправільныя даныя рэз. копіі. Не ўдалося аднавіць.</string>
     <string name="invalid_zip_restore_failed">Аднавіць не атрымалася.Несапраўдны файл рэзервовай копіі: %1$s</string>
     <string name="revocation_explain">Пароль, які вы ўвядзеце тут, дазваляе ануляваць ваш ID на https://myid.threema.ch/revoke у выпадку, калі вы страцілі свой тэлефон ці ён быў скрадзены</string>
+    <!-- Empty state text shown on home screen widget when there are no unread messages -->
     <string name="no_unread_messages">Няма непрачыт. паведамленняў або актывавана блакіроўка PIN</string>
     <string name="send_media">Адпр. мультымед</string>
     <string name="rotate">Паварот</string>
@@ -647,7 +648,7 @@
     <string name="message_declined">\"Не да спадобы\" адпраўлена</string>
     <string name="notifications_settings">Налады апавяшчэнняў</string>
     <string name="notifications_default">Прадвызначаны налады</string>
-    <string name="notifications_until">Да% s</string>
+    <string name="notifications_until">Да %s</string>
     <string name="notifications_mute">Прыглушаны</string>
     <string name="notifications_choose_sound">Выбраць гук</string>
     <string name="error_video_conversion">Памылка канвертавання відэа.</string>
@@ -1684,7 +1685,7 @@
     <!-- ${app_name} is a placeholder for the app's name, e.g. "Threema". It must be left as-is and not translated. -->
     <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 няправільна захаваны на гэтай прыладзе і перашкаджае падключэнню новай прылады. Звярніцеся ў службу падтрымкі («Галоўнае меню > Даведка»).</string>
+    <string name="device_linking_error_invalid_contact_body">Кантакт з ідэнтыфікацыйным нумарам %1$s няправільна захаваны на гэтай прыладзе і перашкаджае падключэнню новай прылады. Звярніцеся ў службу падтрымкі («Галоўнае меню &gt; Даведка»).</string>
     <string name="device_linking_error_screen_close">Закрыць</string>
     <string name="work_directory_title">Даведнік кампаній</string>
     <!-- Instruction text for the user directory search -->
@@ -1699,19 +1700,18 @@
     <string name="contact_availability_status_unavailable">Недаступна</string>
     <!-- Error message (only relevant for OnPrem) if the preset server url is different from the one provided by mdm. -->
     <!-- Error message (only relevant for OnPrem) if the preset server url is different from the one provided by the activation link. -->
-    <!-- Title shown in "About" screen above item that indicates whether the DualLock (a.k.a. "Remote Secret") feature is currently activated. -->
     <!-- 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. -->
-    <!-- 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. -->
-    <!-- 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. -->
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated -->
+    <!-- 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. -->
+    <!-- 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. -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been activated -->
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been deactivated -->
     <!-- Button label, shown on notification that informs user that the DualLock (a.k.a. "Remote Secret") feature has been activated -->
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated -->
-    <!-- Error message shown fullscreen when activating the DualLock (a.k.a. "Remote Secret") feature failed -->
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated -->
-    <!-- Error message shown fullscreen when deactivating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
+    <!-- Error message shown fullscreen when activating 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. -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
+    <!-- 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. -->
     <plurals name="contacts_counter_label">
         <item quantity="one">%d кантакт</item>
         <item quantity="few">%d кантакты</item>

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

@@ -545,6 +545,7 @@
     <string name="invalid_backup">Невалидно резервно копие. Възстановяването не може да се извърши.</string>
     <string name="invalid_zip_restore_failed">Възстановяването се провали. Невалидно резервно копие: %1$s</string>
     <string name="revocation_explain">Паролата, която въвеждате тук, Ви позволява да анулирате Вашия идентификатор на        https://myid.threema.ch/revoke в случай, че го изгубите или е бил откраднат</string>
+    <!-- Empty state text shown on home screen widget when there are no unread messages -->
     <string name="no_unread_messages">Няма непрочетени съобщения или има активно заключване с PIN-код</string>
     <string name="send_media">Изпратете мултимедия</string>
     <string name="rotate">Завъртете</string>
@@ -1707,7 +1708,7 @@
     <string name="emoji_reactions_popup_hint_text">Това съобщение има реакции с емотикони. Натиснете и задръжте емоджи балончетата, за да ги видите всичките.</string>
     <string name="accessibility_device_linking_pfs_warning_icon">Информация</string>
     <!-- The text in [square brackets] must be translated, and the brackets must be kept around the translation. -->
-    <string name="device_linking_pfs_warning_body">Когато свържете устройство, Perfect Forward Secrecy ще бъде деактивирана за всички контакти.\n\nТова е ограничение за настоящата бета версия. За да разберете по-подробно за ограниченията, консултирайте се тук {this FAQ entry}.</string>
+    <string name="device_linking_pfs_warning_body">Когато свържете устройство, Perfect Forward Secrecy ще бъде деактивирана за всички контакти.\n\nТова е ограничение за настоящата бета версия. За да разберете по-подробно за ограниченията, консултирайте се тук [this FAQ entry].</string>
     <string name="device_linking_pfs_warning_button_continue">Свържете ново устройство</string>
     <string name="device_linking_pfs_warning_button_cancel">Отменете</string>
     <!-- Title shown on a banner shown in the old Threema web sessions screen to point to the new multi device feature instead -->
@@ -1755,19 +1756,18 @@
     <string name="error_preset_onprem_url_mismatch_mdm">Предоставеният адрес на сървъра е невалиден. Моля, свържете се с Вашия администратор.</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>
-    <!-- Title shown in "About" screen above item that indicates whether the DualLock (a.k.a. "Remote Secret") feature is currently activated. -->
     <!-- 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. -->
-    <!-- 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. -->
-    <!-- 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. -->
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated -->
+    <!-- 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. -->
+    <!-- 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. -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been activated -->
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been deactivated -->
     <!-- Button label, shown on notification that informs user that the DualLock (a.k.a. "Remote Secret") feature has been activated -->
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated -->
-    <!-- Error message shown fullscreen when activating the DualLock (a.k.a. "Remote Secret") feature failed -->
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated -->
-    <!-- Error message shown fullscreen when deactivating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
+    <!-- Error message shown fullscreen when activating 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. -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
+    <!-- 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. -->
     <plurals name="contacts_counter_label">
         <item quantity="one">%d контакт</item>
         <item quantity="other">%d контакти</item>

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

@@ -512,6 +512,7 @@ de contacte</string>
     <string name="wallpapers_removed">Fons de pantalla eliminats</string>
     <string name="invalid_backup">Dades de còpia no vàlides. No es pot restaurar.</string>
     <string name="revocation_explain">La contrasenya que entreu aquí us permet revocar la vostra ID a https://myid.threema.ch/revoke en cas que la perdeu o us la robin</string>
+    <!-- Empty state text shown on home screen widget when there are no unread messages -->
     <string name="no_unread_messages">Cap missatge per llegir o el bloqueig per PIN activat</string>
     <string name="send_media">Enviar multimèdia</string>
     <string name="rotate">Girar</string>
@@ -1381,19 +1382,18 @@ Si esteu canviant a un dispositiu nou, desinstal·leu o desactiveu %s al disposi
     <!-- Status of a contact (only relevant for Work), e.g. as shown on their profile, indicating that the person is currently not available, i.e., busy or out-of-office -->
     <!-- Error message (only relevant for OnPrem) if the preset server url is different from the one provided by mdm. -->
     <!-- Error message (only relevant for OnPrem) if the preset server url is different from the one provided by the activation link. -->
-    <!-- Title shown in "About" screen above item that indicates whether the DualLock (a.k.a. "Remote Secret") feature is currently activated. -->
     <!-- 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. -->
-    <!-- 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. -->
-    <!-- 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. -->
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated -->
+    <!-- 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. -->
+    <!-- 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. -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been activated -->
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been deactivated -->
     <!-- Button label, shown on notification that informs user that the DualLock (a.k.a. "Remote Secret") feature has been activated -->
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated -->
-    <!-- Error message shown fullscreen when activating the DualLock (a.k.a. "Remote Secret") feature failed -->
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated -->
-    <!-- Error message shown fullscreen when deactivating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
+    <!-- Error message shown fullscreen when activating 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. -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
+    <!-- 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. -->
     <plurals name="contacts_counter_label">
         <item quantity="one">%d contacte</item>
         <item quantity="other">%d contactes</item>

+ 10 - 9
app/src/main/res/values-cs/strings.xml

@@ -546,7 +546,8 @@ http://www.7-zip.org nebo https://itunes.apple.com/us/app/the-unarchiver/id42542
     <string name="invalid_zip_restore_failed">Obnova selhala. Neplatný soubor se zálohou: %1$s</string>
     <string name="revocation_explain">V případě nutnosti zrušit vaše ${app_name_short} ID (například v případě ztráty nebo odcizení) použijte tento odkaz, kde zadáte ID a zde vytvořené heslo.
 https://myid.threema.ch/revoke</string>
-    <string name="no_unread_messages">Žádné nepřečtené zprávy nebo je aktivován zámek PIN</string>
+    <!-- Empty state text shown on home screen widget when there are no unread messages -->
+    <string name="no_unread_messages">Žádné nepřečtené zprávy</string>
     <string name="send_media">Odeslat média</string>
     <string name="rotate">Otočit</string>
     <string name="remove">Odstranit</string>
@@ -1769,27 +1770,27 @@ přátelům vás automaticky najít, pokud vás mají v adresáři svého telef
     <string name="work_intro_screen_onprem_download_private_button_label">Stáhnout</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">Přístup k této aplikaci byl zablokován vaším administrátorem.\n\nPro více informací prosím kontaktujte vašeho administrátora.</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. -->
+    <!-- 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">Token ${remote_secret} se nepodařilo načíst. Zkontrolujte prosím stav vašeho připojení k internetu a zkuste to znovu.</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. -->
+    <!-- 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. -->
     <string name="fetching_remote_secret">Načítání tokenu ${remote_secret}</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated. ${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_activated_notification_title">${remote_secret} byl aktivován</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been activated -->
     <string name="remote_secret_activated_notification_content">Tato funkce udržuje vaše konverzace v bezpečí v případě ztráty nebo odcizení vašeho zařízení.</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated. ${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_deactivated_notification_title">${remote_secret} byl deaktivován</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been deactivated -->
     <string name="remote_secret_deactivated_notification_content">Pokud potřebujete více informací, kontaktujte prosím vašeho administrátora.</string>
     <!-- Button label, shown on notification that informs user that the DualLock (a.k.a. "Remote Secret") feature has been activated -->
     <string name="remote_secret_activated_learn_more">Zjistěte více</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated. ${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_activating">Aktivování ${remote_secret}</string>
-    <!-- Error message shown fullscreen when activating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- Error message shown fullscreen when activating 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_activating_failed">Aktivování ${remote_secret} selhalo. Zkontrolujte prosím stav vašeho připojení k internetu a zkuste to znovu.</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated. ${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">Deaktivování ${remote_secret}</string>
-    <!-- Error message shown fullscreen when deactivating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- 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">Deaktivování ${remote_secret} selhalo. Zkontrolujte prosím stav vašeho připojení k internetu a zkuste to znovu.</string>
     <plurals name="contacts_counter_label">
         <item quantity="one">%d kontakt</item>

+ 10 - 9
app/src/main/res/values-de/strings.xml

@@ -571,7 +571,8 @@ sicheren Ort gesichert oder ausgedruckt haben.</string>
     <string name="invalid_zip_restore_failed">Wiederherstellen fehlgeschlagen. Ungültige Backup-Datei: %1$s</string>
     <string name="revocation_explain">Das Passwort, das Sie hier eingeben, erlaubt Ihnen, Ihre ID unter
         https://myid.threema.ch/revoke zu widerrufen, falls Sie sie verloren haben oder sie gestohlen wurde.</string>
-    <string name="no_unread_messages">Keine ungelesenen Nachrichten oder PIN-Sperre aktiv</string>
+    <!-- Empty state text shown on home screen widget when there are no unread messages -->
+    <string name="no_unread_messages">Keine ungelesenen Nachrichten</string>
     <string name="send_media">Medien senden</string>
     <string name="rotate">Drehen</string>
     <string name="remove">Entfernen</string>
@@ -1797,27 +1798,27 @@ sicheren Ort gesichert oder ausgedruckt haben.</string>
     <string name="work_intro_screen_onprem_download_private_button_label">Jetzt herunterladen</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">Der Zugriff auf diese App wurde von Ihrem Administrator gesperrt.\n\nBitte wenden Sie sich an Ihren Administrator.</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. -->
+    <!-- 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">Das ${remote_secret}-Token konnte nicht abgerufen werden. Bitte prüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.</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. -->
+    <!-- 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. -->
     <string name="fetching_remote_secret">${remote_secret}-Token wird abgerufen</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated. ${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_activated_notification_title">${remote_secret} wurde aktiviert</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been activated -->
     <string name="remote_secret_activated_notification_content">Diese Funktion schützt Ihre Chats, falls Ihr Mobilgerät verloren geht oder gestohlen wird.</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated. ${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_deactivated_notification_title">${remote_secret} wurde deaktiviert</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been deactivated -->
     <string name="remote_secret_deactivated_notification_content">Für weitere Informationen wenden Sie sich bitte an Ihren Administrator.</string>
     <!-- Button label, shown on notification that informs user that the DualLock (a.k.a. "Remote Secret") feature has been activated -->
     <string name="remote_secret_activated_learn_more">Mehr erfahren</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated. ${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_activating">${remote_secret} wird aktiviert</string>
-    <!-- Error message shown fullscreen when activating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- Error message shown fullscreen when activating 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_activating_failed">${remote_secret} konnte nicht aktiviert werden. Bitte prüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated. ${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">${remote_secret} wird deaktiviert</string>
-    <!-- Error message shown fullscreen when deactivating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- 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} konnte nicht deaktiviert werden. Bitte prüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.</string>
     <plurals name="contacts_counter_label">
         <item quantity="one">%d Kontakt</item>

+ 14 - 13
app/src/main/res/values-es/strings.xml

@@ -549,7 +549,8 @@ Introduzca una pregunta para su sondeo.</string>
     <string name="invalid_backup">Copia de seguridad no válida. No se puede restaurar.</string>
     <string name="invalid_zip_restore_failed">La restauración ha fallado. La copia de seguridad no es válida: %1$s</string>
     <string name="revocation_explain">La contraseña introducida aquí le permitirá revocar el ID desde https://myid.threema.ch/revoke en caso de que lo pierda o se lo roben.</string>
-    <string name="no_unread_messages">No hay mensajes sin leer, o está activado el bloqueo con PIN</string>
+    <!-- Empty state text shown on home screen widget when there are no unread messages -->
+    <string name="no_unread_messages">No hay mensajes sin leer</string>
     <string name="send_media">Enviar medios</string>
     <string name="rotate">Rotar</string>
     <string name="remove">Quitar</string>
@@ -1608,7 +1609,7 @@ almacenado.</string>
     <string name="add_shortcut_exists">Ya existe un acceso directo para este objetivo.</string>
     <string name="scan_qr_code">Escanea el código QR</string>
     <!-- ${app_name_desktop} is a placeholder for the desktop app's name, e.g. "Threema". It must be left as-is and not translated. -->
-    <string name="scan_qr_code_explain">Escanea el código QR que se muestra en la versión beta de %s Desktop en el dispositivo que deseas vincular.</string>
+    <string name="scan_qr_code_explain">Escanea el código QR que se muestra en la versión beta de ${app_name_desktop} Desktop en el dispositivo que deseas vincular.</string>
     <string name="connecting">Conectando…</string>
     <string name="trust_new_device">Confía en el nuevo dispositivo</string>
     <string name="trust_new_device_explain">Selecciona las mismas imágenes que se muestran en el nuevo dispositivo para confirmar que confías en él.</string>
@@ -1720,9 +1721,9 @@ almacenado.</string>
     <!-- Screen reader text on a cross icon button to close a hint banner -->
     <string name="accessibility_dismiss_hint">Descartar sugerencia</string>
     <string name="device_linking_error_generic_title">Fallo al enviar datos</string>
-    <string name="device_linking_error_generic_body">Vuelva a intentar vincular el dispositivo. Si el problema persiste, póngase en contacto con el servicio de asistencia («Menú principal > Ayuda»).</string>
+    <string name="device_linking_error_generic_body">Vuelva a intentar vincular el dispositivo. Si el problema persiste, póngase en contacto con el servicio de asistencia («Menú principal &gt; Ayuda»).</string>
     <string name="device_linking_error_generic_network_title">Sin conexión a internet</string>
-    <string name="device_linking_error_generic_network_body">Vuelva a intentar vincular el dispositivo. Si el problema persiste, póngase en contacto con el servicio de asistencia («Menú principal > Ayuda»).</string>
+    <string name="device_linking_error_generic_network_body">Vuelva a intentar vincular el dispositivo. Si el problema persiste, póngase en contacto con el servicio de asistencia («Menú principal &gt; Ayuda»).</string>
     <string name="device_linking_error_unknown_qr_code_title">Código QR desconocido</string>
     <!-- The part of this text that is surrounded by the square brackets will be clickable by the user and lead them to the download page for the desktop client -->
     <!-- The text in [square brackets] must be translated, and the brackets must be kept around the translation. -->
@@ -1737,7 +1738,7 @@ almacenado.</string>
     <string name="device_linking_error_emoji_mismatch_title">¿Las imágenes no coinciden?</string>
     <string name="device_linking_error_emoji_mismatch_body">Parece que hay problemas para conectarse al dispositivo. Reinicie la aplicación en el nuevo dispositivo e inténtelo de nuevo.</string>
     <string name="device_linking_error_unexpected_title">Error inesperado</string>
-    <string name="device_linking_error_unexpected_body">Vuelva a intentar vincular el dispositivo. Si el problema persiste, póngase en contacto con el servicio de asistencia («Menú principal > Ayuda»).</string>
+    <string name="device_linking_error_unexpected_body">Vuelva a intentar vincular el dispositivo. Si el problema persiste, póngase en contacto con el servicio de asistencia («Menú principal &gt; Ayuda»).</string>
     <string name="device_linking_error_invalid_contact_title">El contacto no es válido</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">El contacto con la identidad %1$s no está almacenado correctamente en este dispositivo y hace que se sea posible enlazar con el nuevo dispositivo. Por favor, contacta con el equipo técnico (Menú principal &gt; Ayuda).</string>
@@ -1773,27 +1774,27 @@ almacenado.</string>
     <string name="work_intro_screen_onprem_download_private_button_label">Descargar ahora</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">Su administrador ha bloqueado el acceso a esta aplicación.\n\nPor favor, póngase en contacto con su administrador para más información.</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. -->
+    <!-- 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">No se ha podido recoger la ficha ${remote_secret}. Por favor, compruebe su conexión a Internet e inténtelo de nuevo.</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. -->
+    <!-- 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. -->
     <string name="fetching_remote_secret">Recogiendo la ficha ${remote_secret}</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated. ${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_activated_notification_title">Se ha activado ${remote_secret}</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been activated -->
     <string name="remote_secret_activated_notification_content">Esta función mantiene seguros sus chats si se pierde o le roban el teléfono.</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated. ${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_deactivated_notification_title">Se ha desactivado ${remote_secret}</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been deactivated -->
     <string name="remote_secret_deactivated_notification_content">Si necesita más información, por favor, contacte con su administrador.</string>
     <!-- Button label, shown on notification that informs user that the DualLock (a.k.a. "Remote Secret") feature has been activated -->
     <string name="remote_secret_activated_learn_more">Más información</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated. ${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_activating">Activando ${remote_secret}</string>
-    <!-- Error message shown fullscreen when activating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- Error message shown fullscreen when activating 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_activating_failed">Error al activar ${remote_secret}. Por favor, compruebe su conexión a Internet e inténtelo de nuevo.</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated. ${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">Desactivando ${remote_secret}</string>
-    <!-- Error message shown fullscreen when deactivating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- 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">Error al desactivar ${remote_secret}. Por favor, compruebe su conexión a Internet e inténtelo de nuevo.</string>
     <plurals name="contacts_counter_label">
         <item quantity="one">%d contacto</item>

+ 1 - 1
app/src/main/res/values-es/webclient_strings.xml

@@ -41,5 +41,5 @@
     <string name="webclient_diagnostics_done">Listo. Si tiene problemas al establecer la conexión a la aplicación de escritorio o cliente web, envíe este registro al servicio de asistencia de Threema.</string>
     <string name="webclient_scanned_md_linking_qr_code_title">Código QR incorrecto</string>
     <!-- ${app_name_desktop} is a placeholder for the Desktop app's name, e.g. "Threema". ${md_link_device} is a placeholder for the name of a UI element. They must be left as they are and not translated. -->
-    <string name="webclient_scanned_md_linking_qr_code_message">El código QR que ha escaneado pertenece a la nueva apli beta para escritorio.\n\nVuelva a «Menú principal > ${app_name_desktop} 2.0 para escritorio (Beta)» y seleccione «${md_link_device}» para enlazar con esta apli de escritorio.</string>
+    <string name="webclient_scanned_md_linking_qr_code_message">El código QR que ha escaneado pertenece a la nueva apli beta para escritorio.\n\nVuelva a «Menú principal &gt; ${app_name_desktop} 2.0 para escritorio (Beta)» y seleccione «${md_link_device}» para enlazar con esta apli de escritorio.</string>
 </resources>

+ 11 - 10
app/src/main/res/values-fr/strings.xml

@@ -547,7 +547,8 @@ Veuillez saisir une question pour votre enquête.</string>
     <string name="invalid_backup">Données de sauvegarde invalides. Impossible de restaurer.</string>
     <string name="invalid_zip_restore_failed">Restauration échouée. Fichier de sauvegarde non valide : %1$s</string>
     <string name="revocation_explain">Le mot de passe que vous saisissez ici vous permet de révoquer votre ID sur https://myid.threema.ch/revoke dans le cas où vous le perdez ou vous le faites voler.</string>
-    <string name="no_unread_messages">Aucun message non lu ou verrouillage par NIP activé</string>
+    <!-- Empty state text shown on home screen widget when there are no unread messages -->
+    <string name="no_unread_messages">Aucun message non lu</string>
     <string name="send_media">Envoyer des médias</string>
     <string name="rotate">Pivoter</string>
     <string name="remove">Retirer</string>
@@ -1602,7 +1603,7 @@ Veuillez saisir une question pour votre enquête.</string>
     <string name="add_shortcut_exists">Un raccourci pour cette cible existe déjà.</string>
     <string name="scan_qr_code">Scanner le code QR</string>
     <!-- ${app_name_desktop} is a placeholder for the desktop app's name, e.g. "Threema". It must be left as-is and not translated. -->
-    <string name="scan_qr_code_explain">Scannez le code QR affiché dans la version bêta de %s Desktop sur l’appareil que vous souhaitez associer.</string>
+    <string name="scan_qr_code_explain">Scannez le code QR affiché dans la version bêta de ${app_name_desktop} Desktop sur l’appareil que vous souhaitez associer.</string>
     <string name="connecting">Connexion…</string>
     <string name="trust_new_device">Faire confiance au nouvel appareil</string>
     <string name="trust_new_device_explain">Sélectionnez les mêmes images que celles affichées sur le nouvel appareil pour confirmer que vous lui faites confiance.</string>
@@ -1767,27 +1768,27 @@ Veuillez saisir une question pour votre enquête.</string>
     <string name="work_intro_screen_onprem_download_private_button_label">Télécharger maintenant</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">L’accès à cette application a été bloqué par votre administrateur.\n\nVeuillez le contacter pour obtenir plus d’informations.</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. -->
+    <!-- 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">Le jeton ${remote_secret} n’a pas pu être récupéré. Veuillez vérifier votre connexion Internet et réessayer.</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. -->
+    <!-- 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. -->
     <string name="fetching_remote_secret">Récupération du jeton ${remote_secret}</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated. ${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_activated_notification_title">${remote_secret} a été activé</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been activated -->
     <string name="remote_secret_activated_notification_content">Cette fonctionnalité protège vos conversations en cas de perte ou de vol de votre téléphone.</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated. ${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_deactivated_notification_title">${remote_secret} a été désactivé</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been deactivated -->
     <string name="remote_secret_deactivated_notification_content">Si vous avez besoin de plus d’informations, veuillez contacter votre administrateur.</string>
     <!-- Button label, shown on notification that informs user that the DualLock (a.k.a. "Remote Secret") feature has been activated -->
     <string name="remote_secret_activated_learn_more">En savoir plus</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated. ${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_activating">Activation de ${remote_secret}</string>
-    <!-- Error message shown fullscreen when activating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- Error message shown fullscreen when activating 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_activating_failed">L’activation de ${remote_secret} a échoué. Veuillez vérifier votre connexion Internet et réessayer.</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated. ${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">Désactivation de ${remote_secret}</string>
-    <!-- Error message shown fullscreen when deactivating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- 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">La désactivation de ${remote_secret} a échoué. Veuillez vérifier votre connexion Internet et réessayer.</string>
     <plurals name="contacts_counter_label">
         <item quantity="one">%d contact</item>

+ 38 - 10
app/src/main/res/values-gsw/strings.xml

@@ -544,7 +544,8 @@
     <string name="invalid_backup">D’Backup-Date sind ungültig. Wiederherstelle nöd möglich.</string>
     <string name="invalid_zip_restore_failed">Wiederherstelle fehlgschlage. Ungültigi Backup-Datei: %1$s</string>
     <string name="revocation_explain">S’Passwort, wo Sie da iigebed, erlaubt Ihne, Ihri ID unter https://myid.threema.ch/revoke z’widerrüefe, falls Sie sie verlore händ oder sie gstohle worde isch.</string>
-    <string name="no_unread_messages">Kei ungläseni Nachrichte oder PIN-Sperri aktiv</string>
+    <!-- Empty state text shown on home screen widget when there are no unread messages -->
+    <string name="no_unread_messages">Kei ungläseni Nachrichte</string>
     <string name="send_media">Medie schicke</string>
     <string name="rotate">Drüle</string>
     <string name="remove">Entferne</string>
@@ -687,6 +688,7 @@
     <string name="message_details_message_id_copied">Nachrichte-ID id Zwüscheablag kopiert</string>
     <string name="contact_details_id_copied">ID isch id Zwüscheablag kopiert worde.</string>
     <string name="contact_details_nickname_copied">Nickname isch id Zwüscheablag kopiert worde.</string>
+    <string name="generic_copied_to_clipboard_hint">Id Zwüscheablag kopiert</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="new_wizard_use_id_as_nickname">Wänd Sie Ihri ${app_name_short}-ID als Nickname bruuche?</string>
     <string name="new_wizard_phone_email_invalid">D’Handynummere oder E-Mail-Adrässe, wo Sie ageh händ, isch ungültig. Bitte korrigiered oder lösched Sie die markierte Agabe, bevor Sie wiitermached.</string>
@@ -1100,6 +1102,7 @@
     <string name="username_hint">Username</string>
     <string name="server_hint">Server</string>
     <!-- Shown in "About" to indicate that the DualLock (a.k.a. "Remote Secret") feature is currently activated -->
+    <string name="about_screen_remote_secrets_activated">Aktiviert</string>
     <string name="lock_option_biometric">Biometrisch</string>
     <string name="biometric_enter_authentication">Zum Entsperre bitte authentisiere</string>
     <string name="biometric_authentication_failed">D’Authentisierig isch fehlgschlage</string>
@@ -1743,19 +1746,44 @@
     <string name="error_preset_onprem_url_mismatch_mdm">D’Server-URL isch ungültig. Bitte kontaktiered Sie Ihre Administrator.</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">De Aktivierigslink isch ungültig. Bitte kontaktiered Sie Ihre Administrator.</string>
-    <!-- Title shown in "About" screen above item that indicates whether the DualLock (a.k.a. "Remote Secret") feature is currently activated. -->
+    <string name="work_intro_screen_work_title">HOCHSICHERI KOMMUNIKATION FÜR UNDERNEHME</string>
+    <string name="work_intro_screen_work_subtitle">Threema Work isch di sicher Kommunikationslösig für Undernehme – Ändi-zu-Ändi-verschlüsslet, DSGVO-konform und ide Schwiiz entwicklet.</string>
+    <string name="work_intro_screen_work_sign_in_button_label">Jetzt amälde</string>
+    <string name="work_intro_screen_work_learn_more_hint">Nonig bi Threema Work?</string>
+    <string name="work_intro_screen_work_learn_more_button_label">Meh erfahre</string>
+    <string name="work_intro_screen_work_use_private_hint">Wenn Sie Threema privat bruuche wennd, empfehled mir d’Threema Private-App.</string>
+    <string name="work_intro_screen_work_download_private_button_label">Jetzt abelade</string>
+    <string name="work_intro_screen_onprem_title">KOMMUNIKATION UF HÖCHSTEM SICHERHEITSNIVEAU</string>
+    <string name="work_intro_screen_onprem_subtitle">Threema OnPrem isch di sälberghostet Kommunikationslösig für Undernehme, Behörde und militärischi Irichtige – für vollständigi Datesouveränität, verschlüssleti Dateüberträgig und di höche Aaforderige vo sicherheitsrelevante Bereich.</string>
+    <string name="work_intro_screen_onprem_sign_in_button_label">Jetzt amälde</string>
+    <string name="work_intro_screen_onprem_learn_more_hint">Nonig bi Threema OnPrem?</string>
+    <string name="work_intro_screen_onprem_learn_more_button_label">Meh erfahre</string>
+    <string name="work_intro_screen_onprem_use_private_hint">Wenn Sie Threema privat bruuche wennd, empfehled mir d’Threema Private-App.</string>
+    <string name="work_intro_screen_onprem_download_private_button_label">Jetzt abelade</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. -->
-    <!-- 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. -->
-    <!-- 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. -->
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated -->
+    <string name="remote_secrets_blocked_by_admin">De Zuegriff uf die App isch vo Ihrem Administrator gsperrt worde.\n\nBitte wändet Sie sich a Ihre Administrator.</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">S’${remote_secret}-Token hät nöd chöne abgruefe werde. Bitte prüefed Sie Ihri Internetverbindig und versueched Sie’s nomal.</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. -->
+    <string name="fetching_remote_secret">${remote_secret}-Token wird abgruefe</string>
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated. ${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_activated_notification_title">${remote_secret} isch aktiviert worde</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been activated -->
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <string name="remote_secret_activated_notification_content">Die Funktion schützt Ihri Chats, falls Ihres Handy verlore gaht oder gstohle wird.</string>
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated. ${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_deactivated_notification_title">${remote_secret} isch deaktiviert worde</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <string name="remote_secret_deactivated_notification_content">Für meh Infos wändet Sie sich bitte a Ihre Administrator.</string>
     <!-- Button label, shown on notification that informs user that the DualLock (a.k.a. "Remote Secret") feature has been activated -->
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated -->
-    <!-- Error message shown fullscreen when activating the DualLock (a.k.a. "Remote Secret") feature failed -->
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated -->
-    <!-- Error message shown fullscreen when deactivating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <string name="remote_secret_activated_learn_more">Meh erfahre</string>
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated. ${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_activating">${remote_secret} wird aktiviert</string>
+    <!-- Error message shown fullscreen when activating 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_activating_failed">${remote_secret} hät nöd chöne aktiviert werde. Bitte prüefed Sie Ihri Internetverbindig und versueched Sie’s nomal.</string>
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated. ${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">${remote_secret} wird deaktiviert</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} hät nöd chöne deaktiviert werde. Bitte prüefed Sie Ihri Internetverbindig und versueched Sie’s nomal.</string>
     <plurals name="contacts_counter_label">
         <item quantity="one">%d Kontakt</item>
         <item quantity="other">%d Kontäkt</item>

+ 2 - 0
app/src/main/res/values-gsw/voip_strings.xml

@@ -83,6 +83,7 @@
     <string name="voip_gc_waiting_for_participants">Warte uf anderi Teilnehmer</string>
     <string name="voip_gc_participant_avatar_description">Profilbild vom Teilnehmer</string>
     <string name="voip_gc_participant_mute_status_description">Stummschalt-Status vom Teilnehmer</string>
+    <string name="voip_gc_participant_screen_share_status_description">En Teilnehmer hät en Bildschirm freigeh</string>
     <string name="voip_gc_notification_new_call_public">En neue Gruppearuef isch gstarted worde</string>
     <string name="voip_gc_open_call">Ufmache</string>
     <string name="voip_gc_call_started">Gruppearuef isch gstarted worde</string>
@@ -92,6 +93,7 @@
     <string name="voip_gc_call_error">Gruppearuef isch wägeme Fehler beendet worde</string>
     <string name="voip_gc_call_start_error">Gruppearuef hät nöd chöne gstarted werde</string>
     <string name="voip_gc_call_already_ended">Gruppearuef isch scho beendet worde</string>
+    <string name="voip_gc_screen_sharing_not_supported">D’Bildschirmfreigab isch aktuell nur uf Desktop-Grät verfüegbar.</string>
     <string name="voip_gc_call_full_generic">Kei Teilnahm möglich: Die maximal Teilnehmerzahl isch erreicht.</string>
     <string name="voip_gc_call_full_n">Kei Teilnahm möglich: Di maximal Teilnehmerzahl (%1$d) isch erreicht.</string>
     <plurals name="n_participants_in_call">

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

@@ -524,6 +524,7 @@
     <string name="invalid_backup">A biztonsági másolat adatai érvénytelenek. Visszaállítás nem lehetséges.</string>
     <string name="revocation_explain">Az itt megadott jelszóval visszavonhatja ID-jét a
 		https://myid.threema.ch/revoke címen, ha elveszítette vagy ellopták.</string>
+    <!-- Empty state text shown on home screen widget when there are no unread messages -->
     <string name="no_unread_messages">Nincsenek olvasatlan üzenetek vagy a PIN-zár aktív</string>
     <string name="send_media">Média küldése</string>
     <string name="rotate">Elforgatni</string>
@@ -1391,19 +1392,18 @@ Ha új eszközre vált, kérjük, távolítsa el vagy deaktiválja a %s-t a rég
     <!-- Status of a contact (only relevant for Work), e.g. as shown on their profile, indicating that the person is currently not available, i.e., busy or out-of-office -->
     <!-- Error message (only relevant for OnPrem) if the preset server url is different from the one provided by mdm. -->
     <!-- Error message (only relevant for OnPrem) if the preset server url is different from the one provided by the activation link. -->
-    <!-- Title shown in "About" screen above item that indicates whether the DualLock (a.k.a. "Remote Secret") feature is currently activated. -->
     <!-- 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. -->
-    <!-- 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. -->
-    <!-- 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. -->
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated -->
+    <!-- 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. -->
+    <!-- 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. -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been activated -->
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been deactivated -->
     <!-- Button label, shown on notification that informs user that the DualLock (a.k.a. "Remote Secret") feature has been activated -->
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated -->
-    <!-- Error message shown fullscreen when activating the DualLock (a.k.a. "Remote Secret") feature failed -->
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated -->
-    <!-- Error message shown fullscreen when deactivating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
+    <!-- Error message shown fullscreen when activating 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. -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
+    <!-- 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. -->
     <plurals name="contacts_counter_label">
         <item quantity="one">%d névjegyek</item>
         <item quantity="other">%d névjegyek</item>

+ 10 - 9
app/src/main/res/values-it/strings.xml

@@ -566,7 +566,8 @@ automaticamente in caso di inattività dopo un intervallo predefinito (solo cara
     <string name="invalid_backup">I dati del backup Android non sono validi. Impossibile procedere al ripristino.</string>
     <string name="invalid_zip_restore_failed">Ripristino non riuscito. File di backup non valido: %1$s</string>
     <string name="revocation_explain">La password qui inserita ti permetterà di revocare il tuo ID all\'indirizzo https://myid.threema.ch/revoke in caso di smarrimento o furto della password stessa</string>
-    <string name="no_unread_messages">Non sono presenti messaggi non letti o il blocco PIN è attivo</string>
+    <!-- Empty state text shown on home screen widget when there are no unread messages -->
+    <string name="no_unread_messages">Nessun messaggio non letto</string>
     <string name="send_media">Invia media</string>
     <string name="rotate">Ruota</string>
     <string name="remove">Rimuovi</string>
@@ -1784,27 +1785,27 @@ automaticamente in caso di inattività dopo un intervallo predefinito (solo cara
     <string name="work_intro_screen_onprem_download_private_button_label">Scarica subito</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">L\'accesso a questa app è stato bloccato dal tuo amministratore.\n\nPer ulteriori informazioni, contatta il tuo amministratore.</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. -->
+    <!-- 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">Impossibile recuperare il token ${remote_secret}. Controlla la tua connessione Internet e riprova.</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. -->
+    <!-- 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. -->
     <string name="fetching_remote_secret">Recupero del token ${remote_secret}</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated. ${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_activated_notification_title">${remote_secret} è stato attivato</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been activated -->
     <string name="remote_secret_activated_notification_content">Questa funzione protegge le tue chat in caso di smarrimento o furto del telefono.</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated. ${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_deactivated_notification_title">${remote_secret} è stato disattivato</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been deactivated -->
     <string name="remote_secret_deactivated_notification_content">Per ulteriori informazioni, contatta l\'amministratore.</string>
     <!-- Button label, shown on notification that informs user that the DualLock (a.k.a. "Remote Secret") feature has been activated -->
     <string name="remote_secret_activated_learn_more">Scopri di più</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated. ${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_activating">Attivazione di ${remote_secret}</string>
-    <!-- Error message shown fullscreen when activating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- Error message shown fullscreen when activating 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_activating_failed">Attivazione di ${remote_secret} non riuscita. Controlla la tua connessione Internet e riprova.</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated. ${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">Disattivazione di ${remote_secret}</string>
-    <!-- Error message shown fullscreen when deactivating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- 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">Disattivazione di ${remote_secret} non riuscita. Controlla la tua connessione Internet e riprova.</string>
     <plurals name="contacts_counter_label">
         <item quantity="one">%d contatto</item>

+ 17 - 9
app/src/main/res/values-ja/strings.xml

@@ -61,6 +61,7 @@
     <string name="prefs_vibrate">バイブレーション</string>
     <string name="prefs_sum_vibrate">メッセージを受信するとバイブレーションを鳴らします</string>
     <!-- Label on checkbox in notification settings screen, to let the user enable/disable the use of the device's color LED for notifications. Only relevant on Android 7 -->
+    <string name="prefs_notification_light">通知ライト</string>
     <!-- Summary on checkbox in notification settings screen, to let the user enable/disable the use of the device's color LED for notifications. Only relevant on Android 7 -->
     <string name="prefs_sum_light">白</string>
     <string name="prefs_title_wallpaper">壁紙を選ぶ</string>
@@ -349,6 +350,7 @@ https://shop.threema.ch/retrieve_keys]]></string>
     <string name="creating_group">グループを作成中です</string>
     <string name="updating_group">グループを更新中です</string>
     <string name="leaving_group">グループを退会</string>
+    <string name="removing_group">グループを削除</string>
     <string name="status_create_group">グループが作成されました</string>
     <string name="status_rename_group">グループ名が «%1$s» に変更されました</string>
     <string name="status_group_new_photo">グループ画像が更新されました。</string>
@@ -500,6 +502,7 @@ http://www.7-zip.org または https://itunes.apple.com/us/app/the-unarchiver/id
     <string name="ballot_wizard0_explain">${app_name_short}で投票を行いましょう。イベントのスケジュール、アンケートの作成、友達に聞きたいことなど用途は様々です。</string>
     <string name="ballot_add_choices">回答を追加</string>
     <string name="blocked_cannot_send">連絡先にブロックされているため、メッセージを送信できません</string>
+    <string name="invalid_some_cannot_send">何名かの受信者の選択ができませんでした</string>
     <string name="really_block_contact">これからこの連絡先から送られてくるメッセージは破棄されます。続行しますか?</string>
     <string name="ballot_result_final">最終結果</string>
     <string name="invalid_cannot_send">連絡先が非アクティブのため、メッセージを送信できません</string>
@@ -540,6 +543,7 @@ http://www.7-zip.org または https://itunes.apple.com/us/app/the-unarchiver/id
     <string name="invalid_zip_restore_failed">復元は失敗しました。無効なバックアップファイル: %1$s</string>
     <string name="revocation_explain">ID を紛失したり、盗難された場合に、ここに入力したパスワードを
 https://myid.threema.ch/revoke に入力することで ID を削除することができます。</string>
+    <!-- Empty state text shown on home screen widget when there are no unread messages -->
     <string name="no_unread_messages">未読のメッセージが無いか、PINロックがかかっています</string>
     <string name="send_media">メディアを送信</string>
     <string name="rotate">回転</string>
@@ -552,6 +556,7 @@ https://myid.threema.ch/revoke に入力することで ID を削除すること
     <string name="file_placeholder">ファイル</string>
     <string name="internal_storage">内部ストレージ</string>
     <string name="no_activity_for_mime_type">このファイルを開くためのアプリが見つかりませんでした</string>
+    <string name="no_activity_for_intent">アプリが見つかりません</string>
     <string name="open_from">で開く</string>
     <string name="file_one_contact_not_supported">%1$sファイルを受信できません</string>
     <string name="file_x_contact_not_supported">警告: %1$d 連絡先がファイルを受信できません。</string>
@@ -681,6 +686,7 @@ https://myid.threema.ch/revoke に入力することで ID を削除すること
     <string name="message_details_message_id_copied">メッセージIDがクリップボードにコピーされました</string>
     <string name="contact_details_id_copied">IDをクリップボードにコピーしました</string>
     <string name="contact_details_nickname_copied">ユーザー名をクリップボードにコピーしました</string>
+    <string name="generic_copied_to_clipboard_hint">クリップボードにコピーしました</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="new_wizard_use_id_as_nickname">あなたの${app_name_short} IDをユーザー名として表示させますか?</string>
     <string name="new_wizard_phone_email_invalid">入力された電話番号またはメールアドレスのどちらかが有効ではありません。進む前に修正してください。</string>
@@ -1075,6 +1081,7 @@ https://myid.threema.ch/revoke に入力することで ID を削除すること
     <string name="username_hint">ユーザー名</string>
     <string name="server_hint">サーバー</string>
     <!-- Shown in "About" to indicate that the DualLock (a.k.a. "Remote Secret") feature is currently activated -->
+    <string name="about_screen_remote_secrets_activated">アーカイブ済</string>
     <string name="lock_option_biometric">生体認証</string>
     <string name="biometric_enter_authentication">ロックを解除するために認証してください</string>
     <string name="biometric_authentication_failed">認証に失敗しました</string>
@@ -1566,7 +1573,9 @@ https://myid.threema.ch/revoke に入力することで ID を削除すること
     <string name="add_shortcut_exists">この対象のためのショートカットは既に存在します。</string>
     <string name="scan_qr_code">QRコードをスキャンする</string>
     <!-- ${app_name_desktop} is a placeholder for the desktop app's name, e.g. "Threema". It must be left as-is and not translated. -->
+    <string name="connecting">接続中…</string>
     <string name="trust_new_device">新しい端末を信頼する</string>
+    <string name="no_match_question">マッチしませんか?</string>
     <string name="sending_data">データを送信中…</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="resend_message_dialog_title">グループメッセージを再送信しますか?</string>
@@ -1615,19 +1624,18 @@ https://myid.threema.ch/revoke に入力することで ID を削除すること
     <!-- Status of a contact (only relevant for Work), e.g. as shown on their profile, indicating that the person is currently not available, i.e., busy or out-of-office -->
     <!-- Error message (only relevant for OnPrem) if the preset server url is different from the one provided by mdm. -->
     <!-- Error message (only relevant for OnPrem) if the preset server url is different from the one provided by the activation link. -->
-    <!-- Title shown in "About" screen above item that indicates whether the DualLock (a.k.a. "Remote Secret") feature is currently activated. -->
     <!-- 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. -->
-    <!-- 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. -->
-    <!-- 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. -->
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated -->
+    <!-- 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. -->
+    <!-- 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. -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been activated -->
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been deactivated -->
     <!-- Button label, shown on notification that informs user that the DualLock (a.k.a. "Remote Secret") feature has been activated -->
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated -->
-    <!-- Error message shown fullscreen when activating the DualLock (a.k.a. "Remote Secret") feature failed -->
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated -->
-    <!-- Error message shown fullscreen when deactivating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
+    <!-- Error message shown fullscreen when activating 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. -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
+    <!-- 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. -->
     <plurals name="contacts_counter_label">
         <item quantity="other">%d 連絡先</item>
     </plurals>

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

@@ -83,6 +83,7 @@
     <string name="voip_gc_waiting_for_participants">参加者待ち</string>
     <string name="voip_gc_participant_avatar_description">参加者のプロフィール画像</string>
     <string name="voip_gc_participant_mute_status_description">参加者のミュート状態</string>
+    <string name="voip_gc_participant_screen_share_status_description">画面を共有しています</string>
     <string name="voip_gc_notification_new_call_public">新しいグループ通話が開始されました</string>
     <string name="voip_gc_open_call">開く</string>
     <string name="voip_gc_call_started">グループ通話を開始しました</string>

+ 11 - 10
app/src/main/res/values-nl-rNL/strings.xml

@@ -549,7 +549,8 @@ Voer een vraag in voor uw poll.</string>
     <string name="invalid_zip_restore_failed">Herstellen is mislukt. Ongeldig back-upbestand: %1$s</string>
     <string name="revocation_explain">Met het wachtwoord dat u hier invoert, kunt u uw ID intrekken via
 		https://myid.threema.ch/revoke als u het ooit kwijtraakt of wordt gestolen</string>
-    <string name="no_unread_messages">Geen ongelezen berichten of PIN-slot geactiveerd</string>
+    <!-- Empty state text shown on home screen widget when there are no unread messages -->
+    <string name="no_unread_messages">Geen ongelezen berichten</string>
     <string name="send_media">Media versturen</string>
     <string name="rotate">Draaien</string>
     <string name="remove">Verwijderen</string>
@@ -1602,7 +1603,7 @@ Voer een vraag in voor uw poll.</string>
     <string name="add_shortcut_exists">Er bestaat al een snelkoppeling voor dit doel.</string>
     <string name="scan_qr_code">Scan QR-code</string>
     <!-- ${app_name_desktop} is a placeholder for the desktop app's name, e.g. "Threema". It must be left as-is and not translated. -->
-    <string name="scan_qr_code_explain">Scan de QR-code die wordt weergegeven in de bètaversie van %s Desktop op het apparaat dat je wilt koppelen.</string>
+    <string name="scan_qr_code_explain">Scan de QR-code die wordt weergegeven in de bètaversie van ${app_name_desktop} Desktop op het apparaat dat je wilt koppelen.</string>
     <string name="connecting">Verbinden…</string>
     <string name="trust_new_device">Nieuw apparaat vertrouwen</string>
     <string name="trust_new_device_explain">Selecteer dezelfde afbeeldingen als op het nieuwe apparaat om te bevestigen dat je het vertrouwt.</string>
@@ -1767,27 +1768,27 @@ Voer een vraag in voor uw poll.</string>
     <string name="work_intro_screen_onprem_download_private_button_label">Nu downloaden</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">De toegang tot deze app is geblokkeerd door uw beheerder.\n\nNeem contact op met uw beheerder voor meer informatie.</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. -->
+    <!-- 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">Het ${remote_secret}-token kon niet worden opgehaald. Controleer uw internetverbinding en probeer het opnieuw.</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. -->
+    <!-- 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. -->
     <string name="fetching_remote_secret">${remote_secret}-token wordt opgehaald</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated. ${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_activated_notification_title">${remote_secret} is geactiveerd</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been activated -->
     <string name="remote_secret_activated_notification_content">Deze functie beschermt uw chats in het geval van een gestolen of verloren telefoon.</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated. ${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_deactivated_notification_title">${remote_secret} is gedeactiveerd</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been deactivated -->
     <string name="remote_secret_deactivated_notification_content">Neem contact op met uw beheerder als u meer informatie nodig hebt.</string>
     <!-- Button label, shown on notification that informs user that the DualLock (a.k.a. "Remote Secret") feature has been activated -->
     <string name="remote_secret_activated_learn_more">Meer informatie</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated. ${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_activating">${remote_secret} wordt geactiveerd</string>
-    <!-- Error message shown fullscreen when activating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- Error message shown fullscreen when activating 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_activating_failed">Het activeren van ${remote_secret} is niet gelukt. Controleer uw internetverbinding en probeer het opnieuw.</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated. ${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">${remote_secret} wordt gedeactiveerd</string>
-    <!-- Error message shown fullscreen when deactivating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- 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">Het deactiveren van ${remote_secret} is niet gelukt. Controleer uw internetverbinding en probeer het opnieuw.</string>
     <plurals name="contacts_counter_label">
         <item quantity="one">%d contactpersoon</item>

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

@@ -519,6 +519,7 @@ http://www.7-zip.org eller https://itunes.apple.com/us/app/the-unarchiver/id4254
     <string name="invalid_backup">Ugyldig sikkerhetskopi-data. Kan ikke gjenopprette.</string>
     <string name="invalid_zip_restore_failed">Gjenoppretting feilet. Ugyldig gjenopprettingsfil: %1$s</string>
     <string name="revocation_explain">Passordet du skriver inn her gjør at du kan trekke tilbake din ID hos https://myid.threema.ch/revoke i tilfelle du mister det eller det blir stjålet.</string>
+    <!-- Empty state text shown on home screen widget when there are no unread messages -->
     <string name="no_unread_messages">Ingen uleste meldinger eller PIN-lås aktivert.</string>
     <string name="send_media">Send media</string>
     <string name="rotate">Roter</string>
@@ -1583,19 +1584,18 @@ Om du bytter til en ny enhet, vennligst avinstaller eller deaktiver %s på den g
     <!-- Status of a contact (only relevant for Work), e.g. as shown on their profile, indicating that the person is currently not available, i.e., busy or out-of-office -->
     <!-- Error message (only relevant for OnPrem) if the preset server url is different from the one provided by mdm. -->
     <!-- Error message (only relevant for OnPrem) if the preset server url is different from the one provided by the activation link. -->
-    <!-- Title shown in "About" screen above item that indicates whether the DualLock (a.k.a. "Remote Secret") feature is currently activated. -->
     <!-- 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. -->
-    <!-- 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. -->
-    <!-- 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. -->
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated -->
+    <!-- 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. -->
+    <!-- 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. -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been activated -->
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been deactivated -->
     <!-- Button label, shown on notification that informs user that the DualLock (a.k.a. "Remote Secret") feature has been activated -->
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated -->
-    <!-- Error message shown fullscreen when activating the DualLock (a.k.a. "Remote Secret") feature failed -->
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated -->
-    <!-- Error message shown fullscreen when deactivating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
+    <!-- Error message shown fullscreen when activating 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. -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated. ${remote_secret} is a placeholder for a feature name, e.g. "DualLock". It must be left as-is and not translated. -->
+    <!-- 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. -->
     <plurals name="contacts_counter_label">
         <item quantity="one">%d kontakt</item>
         <item quantity="other">%d kontakter</item>

+ 11 - 10
app/src/main/res/values-pl/strings.xml

@@ -548,7 +548,8 @@ Wprowadź pytanie do swojej ankiety.</string>
     <string name="invalid_backup">Nieprawidłowe dane kopii zapasowej. Nie można jej przywrócić.</string>
     <string name="invalid_zip_restore_failed">Przywracanie niepomyślne. Niewłaściwy plik kopii zapasowej: %1$s</string>
     <string name="revocation_explain">Wprowadzone tutaj hasło pozwala Ci unieważnić identyfikator pod adresem https://myid.threema.ch/revoke, na przykład gdy go zgubisz lub gdy zostanie ukradziony.</string>
-    <string name="no_unread_messages">Brak nieprzeczytanych wiadomości lub blokady PIN</string>
+    <!-- Empty state text shown on home screen widget when there are no unread messages -->
+    <string name="no_unread_messages">Brak nieprzeczytanych wiadomości</string>
     <string name="send_media">Wyślij media</string>
     <string name="rotate">Obróć</string>
     <string name="remove">Usuń</string>
@@ -1611,7 +1612,7 @@ podanie wyłącznie imienia lub pseudonimu. Jeśli nie ustawisz pseudonimu, będ
     <string name="add_shortcut_exists">Skrót tego obiektu docelowego już istnieje.</string>
     <string name="scan_qr_code">Zeskanuj kod QR</string>
     <!-- ${app_name_desktop} is a placeholder for the desktop app's name, e.g. "Threema". It must be left as-is and not translated. -->
-    <string name="scan_qr_code_explain">Zeskanuj kod QR wyświetlony w wersji beta desktopowej aplikacji %s na urządzeniu, które chcesz powiązać.</string>
+    <string name="scan_qr_code_explain">Zeskanuj kod QR wyświetlony w wersji beta desktopowej aplikacji ${app_name_desktop} na urządzeniu, które chcesz powiązać.</string>
     <string name="connecting">Łączenie…</string>
     <string name="trust_new_device">Dodaj nowe urządzenie do zaufanych</string>
     <string name="trust_new_device_explain">Aby potwierdzić, że nowe urządzenie jest zaufane, wybierz te same obrazy, które są na nim wyświetlone.</string>
@@ -1778,27 +1779,27 @@ podanie wyłącznie imienia lub pseudonimu. Jeśli nie ustawisz pseudonimu, będ
     <string name="work_intro_screen_onprem_download_private_button_label">Pobierz teraz</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">Administrator zablokował dostęp do tej aplikacji.\n\nAby uzyskać więcej informacji, skontaktuj się z tą osobą.</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. -->
+    <!-- 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">Nie udało się pobrać tokena ${remote_secret}. Sprawdź swoje połączenie internetowe i spróbuj ponownie.</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. -->
+    <!-- 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. -->
     <string name="fetching_remote_secret">Pobieranie tokena ${remote_secret}</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated. ${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_activated_notification_title">Aktywowano funkcję ${remote_secret}</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been activated -->
     <string name="remote_secret_activated_notification_content">Ta funkcja chroni Twoje czaty w przypadku zgubienia lub kradzieży telefonu.</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated. ${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_deactivated_notification_title">Dezaktywowano funkcję ${remote_secret}</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been deactivated -->
     <string name="remote_secret_deactivated_notification_content">Jeśli potrzebujesz uzyskać więcej informacji, skontaktuj się z administratorem.</string>
     <!-- Button label, shown on notification that informs user that the DualLock (a.k.a. "Remote Secret") feature has been activated -->
     <string name="remote_secret_activated_learn_more">Dowiedz się więcej</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated. ${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_activating">Aktywowanie funkcji ${remote_secret}</string>
-    <!-- Error message shown fullscreen when activating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- Error message shown fullscreen when activating 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_activating_failed">Nie udało się aktywować funkcji ${remote_secret}. Sprawdź swoje połączenie internetowe i spróbuj ponownie.</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated. ${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">Dezaktywowanie funkcji ${remote_secret}</string>
-    <!-- Error message shown fullscreen when deactivating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- 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">Nie udało się dezaktywować funkcji ${remote_secret}. Sprawdź swoje połączenie internetowe i spróbuj ponownie.</string>
     <plurals name="contacts_counter_label">
         <item quantity="one">%d kontakt</item>

+ 14 - 13
app/src/main/res/values-pt-rBR/strings.xml

@@ -547,7 +547,8 @@ Por favor, insira uma pergunta para a sua enquete.</string>
     <string name="invalid_backup">Dados de backup inválidos. Não é possível restaurar.</string>
     <string name="invalid_zip_restore_failed">Falha ao restaurar. Arquivo de backup inválido: %1$s</string>
     <string name="revocation_explain">A senha que você digitar aqui permite que você revogue o seu ID em https://myid.threema.ch/revoke caso você perdê-lo ou ele for roubado</string>
-    <string name="no_unread_messages">Nenhuma mensagem não lida ou bloqueio de PIN ativado</string>
+    <!-- Empty state text shown on home screen widget when there are no unread messages -->
+    <string name="no_unread_messages">Nenhuma mensagem não lida</string>
     <string name="send_media">Enviar mídia</string>
     <string name="rotate">Girar</string>
     <string name="remove">Remover</string>
@@ -1603,7 +1604,7 @@ Por favor, insira uma pergunta para a sua enquete.</string>
     <string name="add_shortcut_exists">Já existe um atalho para esta opção.</string>
     <string name="scan_qr_code">Digitalize o código QR</string>
     <!-- ${app_name_desktop} is a placeholder for the desktop app's name, e.g. "Threema". It must be left as-is and not translated. -->
-    <string name="scan_qr_code_explain">Digitalize o código QR exibido na versão beta do Desktop %s no dispositivo que você deseja vincular.</string>
+    <string name="scan_qr_code_explain">Digitalize o código QR exibido na versão beta do Desktop ${app_name_desktop} no dispositivo que você deseja vincular.</string>
     <string name="connecting">Conectando…</string>
     <string name="trust_new_device">Confiar no novo dispositivo</string>
     <string name="trust_new_device_explain">Selecione as mesmas imagens mostradas no novo dispositivo para confirmar que você confia nele.</string>
@@ -1715,9 +1716,9 @@ Por favor, insira uma pergunta para a sua enquete.</string>
     <!-- Screen reader text on a cross icon button to close a hint banner -->
     <string name="accessibility_dismiss_hint">Ignorar dica</string>
     <string name="device_linking_error_generic_title">Falha no envio de dados</string>
-    <string name="device_linking_error_generic_body">Tente vincular o dispositivo novamente. Se o problema persistir, entre em contato com o suporte (“Menu principal > Ajuda”).</string>
+    <string name="device_linking_error_generic_body">Tente vincular o dispositivo novamente. Se o problema persistir, entre em contato com o suporte (“Menu principal &gt; Ajuda”).</string>
     <string name="device_linking_error_generic_network_title">Sem conexão com a Internet</string>
-    <string name="device_linking_error_generic_network_body">Tente vincular o dispositivo novamente. Se o problema persistir, entre em contato com o suporte (“Menu principal > Ajuda”).</string>
+    <string name="device_linking_error_generic_network_body">Tente vincular o dispositivo novamente. Se o problema persistir, entre em contato com o suporte (“Menu principal &gt; Ajuda”).</string>
     <string name="device_linking_error_unknown_qr_code_title">Código QR desconhecido</string>
     <!-- The part of this text that is surrounded by the square brackets will be clickable by the user and lead them to the download page for the desktop client -->
     <!-- The text in [square brackets] must be translated, and the brackets must be kept around the translation. -->
@@ -1732,7 +1733,7 @@ Por favor, insira uma pergunta para a sua enquete.</string>
     <string name="device_linking_error_emoji_mismatch_title">As imagens não coincidem?</string>
     <string name="device_linking_error_emoji_mismatch_body">Parece que há problemas com a conexão com o dispositivo. Reinicie o aplicativo no novo dispositivo e tente novamente.</string>
     <string name="device_linking_error_unexpected_title">Erro inesperado</string>
-    <string name="device_linking_error_unexpected_body">Tente vincular o dispositivo novamente. Se o problema persistir, entre em contato com o suporte (“Menu principal > Ajuda”).</string>
+    <string name="device_linking_error_unexpected_body">Tente vincular o dispositivo novamente. Se o problema persistir, entre em contato com o suporte (“Menu principal &gt; Ajuda”).</string>
     <string name="device_linking_error_invalid_contact_title">Contato inválido</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">O contato com a identidade %1$s está armazenado incorretamente neste dispositivo e impede a vinculação do novo dispositivo. Entre em contato com o suporte (“Menu principal &gt; Ajuda”).</string>
@@ -1768,27 +1769,27 @@ Por favor, insira uma pergunta para a sua enquete.</string>
     <string name="work_intro_screen_onprem_download_private_button_label">Baixe agora</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">O acesso a este aplicativo foi bloqueado pelo seu administrador.\n\nEntre em contato com seu administrador para obter mais informações.</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. -->
+    <!-- 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">Não foi possível obter o token ${remote_secret}. Verifique sua conexão com a internet e tente novamente.</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. -->
+    <!-- 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. -->
     <string name="fetching_remote_secret">Buscando token ${remote_secret}</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated. ${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_activated_notification_title">O ${remote_secret} foi ativado</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been activated -->
     <string name="remote_secret_activated_notification_content">Este recurso mantém seus chats seguros em caso de perda ou roubo do seu telefone.</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated. ${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_deactivated_notification_title">O ${remote_secret} foi desativado</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been deactivated -->
     <string name="remote_secret_deactivated_notification_content">Se precisar de mais informações, entre em contato com o seu administrador.</string>
     <!-- Button label, shown on notification that informs user that the DualLock (a.k.a. "Remote Secret") feature has been activated -->
     <string name="remote_secret_activated_learn_more">Saiba mais</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated. ${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_activating">Ativando o ${remote_secret}</string>
-    <!-- Error message shown fullscreen when activating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- Error message shown fullscreen when activating 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_activating_failed">Falha ao ativar o ${remote_secret}. Verifique sua conexão com a internet e tente novamente.</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated. ${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">Desativando o ${remote_secret}</string>
-    <!-- Error message shown fullscreen when deactivating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- 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">Falha ao desativar o ${remote_secret}. Verifique sua conexão com a internet e tente novamente.</string>
     <plurals name="contacts_counter_label">
         <item quantity="one">%d contato</item>

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

@@ -41,5 +41,5 @@
     <string name="webclient_diagnostics_done">Pronto. Se você tiver problemas com o estabelecimento da conexão com o aplicativo para desktop ou com o cliente web, envie este log para o suporte do Threema.</string>
     <string name="webclient_scanned_md_linking_qr_code_title">Código QR errado</string>
     <!-- ${app_name_desktop} is a placeholder for the Desktop app's name, e.g. "Threema". ${md_link_device} is a placeholder for the name of a UI element. They must be left as they are and not translated. -->
-    <string name="webclient_scanned_md_linking_qr_code_message">O código QR que você digitalizou pertence ao novo aplicativo beta para desktop. \n\nVolte para “Menu principal > ${app_name_desktop} 2.0 para Desktop (Beta)” e selecione “${md_link_device}” para vincular a este aplicativo para desktop.</string>
+    <string name="webclient_scanned_md_linking_qr_code_message">O código QR que você digitalizou pertence ao novo aplicativo beta para desktop. \n\nVolte para “Menu principal &gt; ${app_name_desktop} 2.0 para Desktop (Beta)” e selecione “${md_link_device}” para vincular a este aplicativo para desktop.</string>
 </resources>

+ 11 - 10
app/src/main/res/values-ru/strings.xml

@@ -546,7 +546,8 @@
     <string name="invalid_backup">Неправильные данные резервной копии. Восстановление невозможно.</string>
     <string name="invalid_zip_restore_failed">Не восстановлено. Неправильный файл резервной копии: %1$s</string>
     <string name="revocation_explain">Пароль, который вы введёте здесь, позволит аннулировать ваш ID на https://myid.threema.ch/revoke в случае, если вы потеряете свой телефон или он будет украден</string>
-    <string name="no_unread_messages">Нет непрочитанных сообщений или активирована блокировка PIN-кодом</string>
+    <!-- Empty state text shown on home screen widget when there are no unread messages -->
+    <string name="no_unread_messages">Нет непрочитанных сообщений</string>
     <string name="send_media">Отправка мультимедиа</string>
     <string name="rotate">Вращать</string>
     <string name="remove">Удалить</string>
@@ -1600,7 +1601,7 @@
     <string name="add_shortcut_exists">Ярлык для этого элемента уже существует.</string>
     <string name="scan_qr_code">Сканировать QR-код</string>
     <!-- ${app_name_desktop} is a placeholder for the desktop app's name, e.g. "Threema". It must be left as-is and not translated. -->
-    <string name="scan_qr_code_explain">Отсканируйте QR-код, отображаемый в бета-версии %s для ПК, на устройстве, которое вы хотите привязать.</string>
+    <string name="scan_qr_code_explain">Отсканируйте QR-код, отображаемый в бета-версии ${app_name_desktop} для ПК, на устройстве, которое вы хотите привязать.</string>
     <string name="connecting">Подключение…</string>
     <string name="trust_new_device">Доверять новому устройству</string>
     <string name="trust_new_device_explain">Выберите те же изображения, что и на новом устройстве, чтобы подтвердить доверие.</string>
@@ -1767,27 +1768,27 @@
     <string name="work_intro_screen_onprem_download_private_button_label">Скачать</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>
-    <!-- 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. -->
+    <!-- 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. -->
+    <!-- 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. -->
     <string name="fetching_remote_secret">Получение метки ${remote_secret}</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated. ${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_activated_notification_title">${remote_secret} активирован</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been activated -->
     <string name="remote_secret_activated_notification_content">Эта функция сохранит вашу переписку в безопасности в случае утери или кражи телефона.</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated. ${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_deactivated_notification_title">${remote_secret} деактивирован</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been deactivated -->
     <string name="remote_secret_deactivated_notification_content">Для получения дополнительной информации свяжитесь со своим администратором.</string>
     <!-- Button label, shown on notification that informs user that the DualLock (a.k.a. "Remote Secret") feature has been activated -->
     <string name="remote_secret_activated_learn_more">Узнать больше</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated. ${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_activating">Активация ${remote_secret}</string>
-    <!-- Error message shown fullscreen when activating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- Error message shown fullscreen when activating 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_activating_failed">Невозможно активировать ${remote_secret}. Проверьте соединение с интернетом и повторите попытку.</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated. ${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">Деактивация ${remote_secret}</string>
-    <!-- Error message shown fullscreen when deactivating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- 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>
     <plurals name="contacts_counter_label">
         <item quantity="one">%d контакт</item>

+ 39 - 11
app/src/main/res/values-sk/strings.xml

@@ -549,7 +549,8 @@ Naplánujte udalosti, vytvorte prieskum, alebo sa niečo spýtajte svojich priat
     <string name="invalid_zip_restore_failed">Obnovenie zlyhalo. Neplatný súbor zálohy: %1$s</string>
     <string name="revocation_explain">Heslo, ktoré tu zadáte, vám umožní odvolať vaše ID na stránke
 https://myid.threema.ch/revoke v prípade, že ho stratíte alebo vám bude odcudzené</string>
-    <string name="no_unread_messages">Žiadne neprečítané správy, alebo je aktivovaný zámok PIN</string>
+    <!-- Empty state text shown on home screen widget when there are no unread messages -->
+    <string name="no_unread_messages">Žiadne neprečítané správy</string>
     <string name="send_media">Odoslať média</string>
     <string name="rotate">Otočiť</string>
     <string name="remove">Odstrániť</string>
@@ -693,6 +694,7 @@ Už ich nebude možné obnoviť.</string>
     <string name="message_details_message_id_copied">ID správy skopírované do schránky</string>
     <string name="contact_details_id_copied">ID bolo skopírované do schránky</string>
     <string name="contact_details_nickname_copied">Prezývka bola skopírovaná do schránky</string>
+    <string name="generic_copied_to_clipboard_hint">Skopírované do schránky</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="new_wizard_use_id_as_nickname">Chcete použiť vaše ${app_name_short} ID ako prezývku?</string>
     <string name="new_wizard_phone_email_invalid">Telefónne číslo alebo emailová adresa, ktorú ste zadali, nie sú platné.\nOpravte to a pokračujte.</string>
@@ -1107,6 +1109,7 @@ Vykonajte prosím zálohú vašich údajov vhodnou metódou.</string>
     <string name="username_hint">Používateľské meno</string>
     <string name="server_hint">Server</string>
     <!-- Shown in "About" to indicate that the DualLock (a.k.a. "Remote Secret") feature is currently activated -->
+    <string name="about_screen_remote_secrets_activated">Aktivované</string>
     <string name="lock_option_biometric">Biometrický</string>
     <string name="biometric_enter_authentication">Pre odomknutie vykonajte overenie</string>
     <string name="biometric_authentication_failed">Overenie zlyhalo</string>
@@ -1602,7 +1605,7 @@ Vykonajte prosím zálohú vašich údajov vhodnou metódou.</string>
     <string name="add_shortcut_exists">Odkaz pre tento cieľ už existuje.</string>
     <string name="scan_qr_code">Skenovať QR kód</string>
     <!-- ${app_name_desktop} is a placeholder for the desktop app's name, e.g. "Threema". It must be left as-is and not translated. -->
-    <string name="scan_qr_code_explain">Naskenujte kód QR zobrazený v beta verzii aplikácie %s Desktop na zariadení, ktoré chcete prepojiť.</string>
+    <string name="scan_qr_code_explain">Naskenujte kód QR zobrazený v beta verzii aplikácie ${app_name_desktop} Desktop na zariadení, ktoré chcete prepojiť.</string>
     <string name="connecting">Pripája sa…</string>
     <string name="trust_new_device">Dôverovať novému zariadeniu</string>
     <string name="trust_new_device_explain">Vyberte rovnaké obrázky, aké sú zobrazené na novom zariadení, aby ste potvrdili, že mu dôverujete.</string>
@@ -1754,19 +1757,44 @@ Vykonajte prosím zálohú vašich údajov vhodnou metódou.</string>
     <string name="error_preset_onprem_url_mismatch_mdm">Zadaná URL adresa servera je neplatná. Obráťte sa na správcu.</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">Aktivačný odkaz je neplatný. Obráťte sa na svojho správcu.</string>
-    <!-- Title shown in "About" screen above item that indicates whether the DualLock (a.k.a. "Remote Secret") feature is currently activated. -->
+    <string name="work_intro_screen_work_title">VYSOKO BEZPEČNÁ KOMUNIKÁCIA PRE FIRMY</string>
+    <string name="work_intro_screen_work_subtitle">Threema Work je bezpečné komunikačné riešenie pre firmy – s end-to-end šifrovaním, v súlade s GDPR a vyvinuté vo Švajčiarsku.</string>
+    <string name="work_intro_screen_work_sign_in_button_label">Prihlásiť sa</string>
+    <string name="work_intro_screen_work_learn_more_hint">Ešte nepoužívate Threema Work?</string>
+    <string name="work_intro_screen_work_learn_more_button_label">Zistiť viac</string>
+    <string name="work_intro_screen_work_use_private_hint">Ak chcete používať Threemu na súkromné účely, odporúčame aplikáciu Threema Private.</string>
+    <string name="work_intro_screen_work_download_private_button_label">Stiahnuť teraz</string>
+    <string name="work_intro_screen_onprem_title">KOMUNIKÁCIA NA NAJVYŠŠEJ ÚROVNI BEZPEČNOSTI</string>
+    <string name="work_intro_screen_onprem_subtitle">Threema OnPrem je komunikačné riešenie s vlastným hosťovaním pre podniky, vládne agentúry a vojenské organizácie – pre úplnú suverenitu údajov, šifrovaný prenos údajov a vysoké požiadavky na prostredia citlivé na bezpečnosť.</string>
+    <string name="work_intro_screen_onprem_sign_in_button_label">Prihlásiť sa</string>
+    <string name="work_intro_screen_onprem_learn_more_hint">Ešte nepoužívate Threema OnPrem?</string>
+    <string name="work_intro_screen_onprem_learn_more_button_label">Zistiť viac</string>
+    <string name="work_intro_screen_onprem_use_private_hint">Ak chcete používať Threemu na súkromné účely, odporúčame aplikáciu Threema Private.</string>
+    <string name="work_intro_screen_onprem_download_private_button_label">Stiahnuť teraz</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. -->
-    <!-- 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. -->
-    <!-- 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. -->
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated -->
+    <string name="remote_secrets_blocked_by_admin">Prístup k tejto aplikácii bol blokovaný správcom.\n\nPre viac informácií kontaktujte správcu.</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">Token ${remote_secret} sa nepodarilo načítať. Skontrolujte pripojenie k internetu a skúste to znova.</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. -->
+    <string name="fetching_remote_secret">Načítanie tokenu ${remote_secret}</string>
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated. ${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_activated_notification_title">${remote_secret} bol aktivovaný</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been activated -->
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <string name="remote_secret_activated_notification_content">Táto funkcia chráni vaše konverzácie v prípade straty alebo krádeže telefónu.</string>
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated. ${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_deactivated_notification_title">${remote_secret} bol deaktivovaný</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <string name="remote_secret_deactivated_notification_content">Ak potrebujete viac informácií, kontaktujte svojho správcu.</string>
     <!-- Button label, shown on notification that informs user that the DualLock (a.k.a. "Remote Secret") feature has been activated -->
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated -->
-    <!-- Error message shown fullscreen when activating the DualLock (a.k.a. "Remote Secret") feature failed -->
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated -->
-    <!-- Error message shown fullscreen when deactivating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <string name="remote_secret_activated_learn_more">Zistiť viac</string>
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated. ${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_activating">Aktivácia ${remote_secret}</string>
+    <!-- Error message shown fullscreen when activating 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_activating_failed">Aktivácia ${remote_secret} zlyhala. Skontrolujte pripojenie k internetu a skúste to znova.</string>
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated. ${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">Deaktivácia ${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">Deaktivácia ${remote_secret} zlyhala. Skontrolujte pripojenie k internetu a skúste to znova.</string>
     <plurals name="contacts_counter_label">
         <item quantity="one">%d kontakt</item>
         <item quantity="few">%d kontakty</item>

+ 2 - 0
app/src/main/res/values-sk/voip_strings.xml

@@ -83,6 +83,7 @@
     <string name="voip_gc_waiting_for_participants">Čakanie na účastníkov</string>
     <string name="voip_gc_participant_avatar_description">Profilová fotka účastníka</string>
     <string name="voip_gc_participant_mute_status_description">Stav stlmeného účastníka</string>
+    <string name="voip_gc_participant_screen_share_status_description">Účastník zdieľa obrazovku</string>
     <string name="voip_gc_notification_new_call_public">Bol spustený nový skupinový hovor</string>
     <string name="voip_gc_open_call">Otvoriť</string>
     <string name="voip_gc_call_started">Bol spustený skupinový hovor</string>
@@ -92,6 +93,7 @@
     <string name="voip_gc_call_error">Hovor bol ukončený z dôvodu chyby</string>
     <string name="voip_gc_call_start_error">Skupinový hovor sa nepodarilo spustiť</string>
     <string name="voip_gc_call_already_ended">Skupinový hovor už skončil</string>
+    <string name="voip_gc_screen_sharing_not_supported">Zdieľanie obrazovky je v súčastnosti obmedzené na desktopové zariadenia.</string>
     <string name="voip_gc_call_full_generic">Dosiahlo sa maximum: K tomuto skupinovému hovoru sa nemôžu pripojiť žiadni ďalší účastníci.</string>
     <string name="voip_gc_call_full_n">Dosiahol sa maximálny počet účastníkov (%1$d), k tomuto skupinovému hovoru sa nemôžete pripojiť.</string>
     <plurals name="n_participants_in_call">

+ 2 - 0
app/src/main/res/values-sk/webclient_strings.xml

@@ -39,5 +39,7 @@
     <string name="webclient_diagnostics_start">Spustiť</string>
     <string name="webclient_diagnostics_intro">Stisknutím tlačidla «Spustiť» zahájite test</string>
     <string name="webclient_diagnostics_done">Hotovo. Pokiaľ zaznamenáte problémy s nadviazaním spojenia na desktopovú aplikáciu alebo webového klienta, odošlite prosím tento protokol podpore Threema.</string>
+    <string name="webclient_scanned_md_linking_qr_code_title">Nesprávny QR kód</string>
     <!-- ${app_name_desktop} is a placeholder for the Desktop app's name, e.g. "Threema". ${md_link_device} is a placeholder for the name of a UI element. They must be left as they are and not translated. -->
+    <string name="webclient_scanned_md_linking_qr_code_message">QR kód, ktorý ste naskenovali, patrí k novej beta verzii aplikácie pre počítače. \n\nVráťte sa späť do „Hlavné menu &gt; ${app_name_desktop} 2.0 pre počítače (Beta)“ a vyberte „${md_link_device}“, aby ste sa pripojili k tejto aplikácii pre počítače.</string>
 </resources>

+ 11 - 10
app/src/main/res/values-tr/strings.xml

@@ -545,7 +545,8 @@ tekrar denemeden önce girdiğiniz numaranın doğru olduğundan ve mobil ağa b
     <string name="invalid_backup">Geçersiz yedekleme verisi. Geri yüklenemiyor.</string>
     <string name="invalid_zip_restore_failed">Geri yükleme başarısız oldu. Geçersiz yedekleme dosyası: %1$s</string>
     <string name="revocation_explain">Buraya girdiğiniz şifre, kaybetmeniz veya çalınması durumunda kimliğinizi https://myid.threema.ch/revoke adresinden iptal etmenizi sağlar.</string>
-    <string name="no_unread_messages">Okunmamış mesaj yok veya PIN kilidi etkinleştirildi</string>
+    <!-- Empty state text shown on home screen widget when there are no unread messages -->
+    <string name="no_unread_messages">Okunmamış mesaj yok</string>
     <string name="send_media">Medya gönder</string>
     <string name="rotate">Döndür</string>
     <string name="remove">Kaldır</string>
@@ -1598,7 +1599,7 @@ sunucularımıza güvenli bir şekilde iletildi. Kişisel anahtar hiçbir zaman
     <string name="add_shortcut_exists">Bu hedef için bir kısayol zaten mevcut.</string>
     <string name="scan_qr_code">Kare kodu tara</string>
     <!-- ${app_name_desktop} is a placeholder for the desktop app's name, e.g. "Threema". It must be left as-is and not translated. -->
-    <string name="scan_qr_code_explain">Bağlamak istediğiniz cihazda %s beta sürümünde görüntülenen QR kodunu tarayın.</string>
+    <string name="scan_qr_code_explain">Bağlamak istediğiniz cihazda ${app_name_desktop} beta sürümünde görüntülenen QR kodunu tarayın.</string>
     <string name="connecting">Bağlanıyor…</string>
     <string name="trust_new_device">Yeni Cihaza Güven</string>
     <string name="trust_new_device_explain">Güvendiğinizi onaylamak için yeni cihazda gösterilen resimlerin aynısını seçin.</string>
@@ -1763,27 +1764,27 @@ sunucularımıza güvenli bir şekilde iletildi. Kişisel anahtar hiçbir zaman
     <string name="work_intro_screen_onprem_download_private_button_label">Şimdi indir</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">Bu uygulamaya erişim yöneticiniz tarafından engellendi.\n\nDaha fazla bilgi için lütfen yöneticinizle iletişime geçin.</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. -->
+    <!-- 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} belirteci alınamadı. Lütfen internet bağlantınızı kontrol edip tekrar deneyin.</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. -->
+    <!-- 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. -->
     <string name="fetching_remote_secret">${remote_secret} belirteci getiriliyor</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated. ${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_activated_notification_title">${remote_secret} etkinleştirildi</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been activated -->
     <string name="remote_secret_activated_notification_content">Bu özellik, telefonunuz kaybolduğunda veya çalındığında sohbetlerinizin güvende kalmasını sağlar.</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated. ${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_deactivated_notification_title">${remote_secret} etkinleştirildi</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been deactivated -->
     <string name="remote_secret_deactivated_notification_content">Daha fazla bilgiye ihtiyacınız varsa lütfen yöneticinizle iletişime geçin.</string>
     <!-- Button label, shown on notification that informs user that the DualLock (a.k.a. "Remote Secret") feature has been activated -->
     <string name="remote_secret_activated_learn_more">Daha fazla bilgi edin</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated. ${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_activating">${remote_secret}\'u Etkinleştirme</string>
-    <!-- Error message shown fullscreen when activating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- Error message shown fullscreen when activating 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_activating_failed">${remote_secret} etkinleştirilemedi. Lütfen internet bağlantınızı kontrol edip tekrar deneyin.</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated. ${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">${remote_secret}\'u Etkinleştirme</string>
-    <!-- Error message shown fullscreen when deactivating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- 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} devre dışı bırakılamadı. Lütfen internet bağlantınızı kontrol edip tekrar deneyin.</string>
     <plurals name="contacts_counter_label">
         <item quantity="one">%d kişi</item>

+ 9 - 8
app/src/main/res/values-uk/strings.xml

@@ -565,6 +565,7 @@
     <string name="invalid_zip_restore_failed">Помилка відновлення. Недійсний файл резервної копії: %1$s</string>
     <string name="revocation_explain">Пароль, який ви вводите тут, дає змогу відкликати ваш ID на сторінці
 		https://myid.threema.ch/revoke у випадку його втрати або крадіжки</string>
+    <!-- Empty state text shown on home screen widget when there are no unread messages -->
     <string name="no_unread_messages">Немає непрочитаних повідомлень або активовано блокування PIN-кодом</string>
     <string name="send_media">Надіслати медіафайли</string>
     <string name="rotate">Повернути</string>
@@ -1803,27 +1804,27 @@
     <string name="work_intro_screen_onprem_download_private_button_label">Завантажити</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>
-    <!-- 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. -->
+    <!-- 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. -->
+    <!-- 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. -->
     <string name="fetching_remote_secret">Завантаження маркера ${remote_secret}</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated. ${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_activated_notification_title">${remote_secret} активовано</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been activated -->
     <string name="remote_secret_activated_notification_content">Ця функція захищає ваші чати, коли телефон загублено або вкрадено.</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated. ${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_deactivated_notification_title">${remote_secret} активовано</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been deactivated -->
     <string name="remote_secret_deactivated_notification_content">Щоб дізнатися більше, зверніться до свого адміністратора.</string>
     <!-- Button label, shown on notification that informs user that the DualLock (a.k.a. "Remote Secret") feature has been activated -->
     <string name="remote_secret_activated_learn_more">Докладніше</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated. ${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_activating">Активація ${remote_secret}</string>
-    <!-- Error message shown fullscreen when activating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- Error message shown fullscreen when activating 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_activating_failed">Помилка активації ${remote_secret}. Перевірте інтернет-з\'єднання і повторіть спробу.</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated. ${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">Деактивація ${remote_secret}</string>
-    <!-- Error message shown fullscreen when deactivating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- 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>
     <plurals name="contacts_counter_label">
         <item quantity="one">%d контакт</item>

+ 11 - 10
app/src/main/res/values-zh-rCN/strings.xml

@@ -559,7 +559,8 @@ http://www.7-zip.org 或 https://itunes.apple.com/us/app/the-unarchiver/id425424
     <string name="invalid_backup">无效的备份数据。无法恢复。</string>
     <string name="invalid_zip_restore_failed">恢复失败,这是无效的备份数据:%1$s</string>
     <string name="revocation_explain">您在这里输入的密码,可让您通过 https://myid.threema.ch/revoke 吊销您的ID,以防丢失或被盗</string>
-    <string name="no_unread_messages">没有未读消息或 PIN 锁已激活</string>
+    <!-- Empty state text shown on home screen widget when there are no unread messages -->
+    <string name="no_unread_messages">没有未读消息</string>
     <string name="send_media">发送媒体</string>
     <string name="rotate">旋转</string>
     <string name="remove">删除</string>
@@ -1625,7 +1626,7 @@ ${app_name_short} 支持的所有表情符号。</string>
     <string name="add_shortcut_exists">此目标的快捷方式已经存在。</string>
     <string name="scan_qr_code">扫描二维码</string>
     <!-- ${app_name_desktop} is a placeholder for the desktop app's name, e.g. "Threema". It must be left as-is and not translated. -->
-    <string name="scan_qr_code_explain">在您要链接的设备上扫描 %s 桌面测试版中显示的二维码。</string>
+    <string name="scan_qr_code_explain">在您要链接的设备上扫描 ${app_name_desktop} 桌面测试版中显示的二维码。</string>
     <string name="connecting">连接中…</string>
     <string name="trust_new_device">信任新设备</string>
     <string name="trust_new_device_explain">请选择与新设备上显示的相同图像,以确认您信任新设备。</string>
@@ -1789,27 +1790,27 @@ ${app_name_short} 支持的所有表情符号。</string>
     <string name="work_intro_screen_onprem_download_private_button_label">立刻下载</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>
-    <!-- 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. -->
+    <!-- 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. -->
+    <!-- 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. -->
     <string name="fetching_remote_secret">获取 ${remote_secret} 令牌</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated. ${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_activated_notification_title">${remote_secret} 已启用</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been activated -->
     <string name="remote_secret_activated_notification_content">此功能可在手机丢失或被盗时保障您的聊天内容安全。</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated. ${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_deactivated_notification_title">${remote_secret} 已停用</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been deactivated -->
     <string name="remote_secret_deactivated_notification_content">如需更多信息,请联系您的管理员。</string>
     <!-- Button label, shown on notification that informs user that the DualLock (a.k.a. "Remote Secret") feature has been activated -->
     <string name="remote_secret_activated_learn_more">了解更多</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated. ${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_activating">正在启用 ${remote_secret}</string>
-    <!-- Error message shown fullscreen when activating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- Error message shown fullscreen when activating 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_activating_failed">启用 ${remote_secret} 失败,请检查您的互联网连接并重试。</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated. ${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">正在停用 ${remote_secret}</string>
-    <!-- Error message shown fullscreen when deactivating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- 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>
     <plurals name="contacts_counter_label">
         <item quantity="other">%d个联系人</item>

+ 11 - 10
app/src/main/res/values-zh-rTW/strings.xml

@@ -559,7 +559,8 @@ http://www.7-zip.org 或 https://itunes.apple.com/us/app/the-unarchiver/id425424
     <string name="invalid_backup">無效的備份資料。無法還原。</string>
     <string name="invalid_zip_restore_failed">還原失敗,這是無效的備份資料:%1$s</string>
     <string name="revocation_explain">您在這裡輸入的密碼,可讓您透過 https://myid.threema.ch/revoke 撤銷您的ID,以防丟失或被盜</string>
-    <string name="no_unread_messages">沒有未讀訊息或 PIN 鎖被啟動</string>
+    <!-- Empty state text shown on home screen widget when there are no unread messages -->
+    <string name="no_unread_messages">沒有未讀訊息</string>
     <string name="send_media">傳送媒體</string>
     <string name="rotate">旋轉</string>
     <string name="remove">移除</string>
@@ -1624,7 +1625,7 @@ ${app_name_short} 支援的所有表情符號。</string>
     <string name="add_shortcut_exists">此目標的捷徑已經存在。</string>
     <string name="scan_qr_code">掃描QR碼</string>
     <!-- ${app_name_desktop} is a placeholder for the desktop app's name, e.g. "Threema". It must be left as-is and not translated. -->
-    <string name="scan_qr_code_explain">在您要連結的裝置上掃描 %s 桌面測試版中顯示的QR碼。</string>
+    <string name="scan_qr_code_explain">在您要連結的裝置上掃描 ${app_name_desktop} 桌面測試版中顯示的QR碼。</string>
     <string name="connecting">連線中…</string>
     <string name="trust_new_device">信任新裝置</string>
     <string name="trust_new_device_explain">請選擇與新裝置上顯示的相同圖像,以確認您信任新裝置。</string>
@@ -1788,27 +1789,27 @@ ${app_name_short} 支援的所有表情符號。</string>
     <string name="work_intro_screen_onprem_download_private_button_label">立刻下載</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>
-    <!-- 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. -->
+    <!-- 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. -->
+    <!-- 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. -->
     <string name="fetching_remote_secret">取得 ${remote_secret} 權杖</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been activated. ${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_activated_notification_title">${remote_secret} 已啟用</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been activated -->
     <string name="remote_secret_activated_notification_content">此功能可在手機遺失或遭竊時保護您的對話內容安全。</string>
-    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated -->
+    <!-- Shown as the notification title when DualLock (a.k.a. "Remote Secret") has been deactivated. ${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_deactivated_notification_title">${remote_secret} 已停用</string>
     <!-- Shown as the notification content when DualLock (a.k.a. "Remote Secret") has been deactivated -->
     <string name="remote_secret_deactivated_notification_content">如需更多資訊,請聯絡您的管理員。</string>
     <!-- Button label, shown on notification that informs user that the DualLock (a.k.a. "Remote Secret") feature has been activated -->
     <string name="remote_secret_activated_learn_more">了解更多</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being activated. ${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_activating">正在啟用 ${remote_secret}</string>
-    <!-- Error message shown fullscreen when activating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- Error message shown fullscreen when activating 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_activating_failed">啟用 ${remote_secret} 失敗,請檢查您的網路連線並再試一次。</string>
-    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated -->
+    <!-- Shown on loading indicator while the DualLock (a.k.a. "Remote Secret") feature is being deactivated. ${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">正在停用 ${remote_secret}</string>
-    <!-- Error message shown fullscreen when deactivating the DualLock (a.k.a. "Remote Secret") feature failed -->
+    <!-- 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>
     <plurals name="contacts_counter_label">
         <item quantity="other">%d位聯絡人</item>

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

@@ -564,7 +564,8 @@
     <string name="invalid_zip_restore_failed">Restore failed. Invalid backup file: %1$s</string>
     <string name="revocation_explain">The password you enter here allows you to revoke your ID at
         https://myid.threema.ch/revoke in case you lose it or it gets stolen</string>
-    <string name="no_unread_messages">No unread messages or PIN lock activated</string>
+    <!-- Empty state text shown on home screen widget when there are no unread messages -->
+    <string name="no_unread_messages">No unread messages</string>
     <string name="send_media">Send media</string>
     <string name="rotate">Rotate</string>
     <string name="remove">Remove</string>

+ 1 - 2
app/src/main/res/values/untranslatable_strings.xml

@@ -13,11 +13,10 @@
     <!-- The marketing name of the remote secret feature -->
     <string name="remote_secret" translatable="false">DualLock</string>
     <string name="remote_secret_learn_more_url" translatable="false">https://threema.com/faq/duallock</string>
-    <string name="threema_safe" translatable="false">Threema Safe</string>
+    <string name="threema_safe" translatable="false">${app_name_short} Safe</string>
     <string name="safe_enter_server_address_explain" translatable="false">server.example.com</string>
     <string name="lp_map_copyright" translatable="false">© Threema © MapTiler © OpenStreetMap contributors</string>
     <string name="lp_poi_copyright" translatable="false">Data © Threema © OpenStreetMap contributors</string>
-    <string name="capture_name" translatable="false">Threema QR Code Scanner</string>
     <string name="prefs_work_time_start_sum_time" translatable="false">%s</string>
     <string name="prefs_work_time_end_sum_time" translatable="false">%s</string>
     <string name="audio_focus_loss" translatable="false">Audio focus loss</string>

+ 74 - 0
app/src/test/java/ch/threema/base/FileExtensionsTest.kt

@@ -0,0 +1,74 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2025 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.base
+
+import ch.threema.testhelpers.createTempDirectory
+import java.io.File
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertContentEquals
+import kotlin.test.assertFails
+import kotlin.test.assertFalse
+
+class FileExtensionsTest {
+
+    private lateinit var directory: File
+
+    @BeforeTest
+    fun setUp() {
+        directory = createTempDirectory()
+    }
+
+    @AfterTest
+    fun tearDown() {
+        directory.deleteRecursively()
+    }
+
+    @Test
+    fun `write file successfully`() {
+        val file = File(directory, "my-file")
+        assertFalse(file.exists())
+
+        file.writeAtomically { outputStream ->
+            outputStream.write(byteArrayOf(1, 2, 3))
+            outputStream.write(byteArrayOf(4, 5, 6))
+        }
+
+        assertContentEquals(byteArrayOf(1, 2, 3, 4, 5, 6), file.readBytes())
+    }
+
+    @Test
+    fun `write file unsuccessfully`() {
+        val file = File(directory, "my-file")
+        assertFalse(file.exists())
+
+        assertFails {
+            file.writeAtomically { outputStream ->
+                outputStream.write(byteArrayOf(1, 2, 3))
+                error("something went wrong")
+            }
+        }
+
+        assertFalse(file.exists())
+    }
+}

+ 81 - 3
app/src/test/java/ch/threema/localcrypto/MasterKeyStorageManagerTest.kt

@@ -21,13 +21,17 @@
 
 package ch.threema.localcrypto
 
+import ch.threema.localcrypto.MasterKeyTestData.MASTER_KEY
+import ch.threema.localcrypto.models.MasterKeyData
 import ch.threema.localcrypto.models.MasterKeyState
 import ch.threema.localcrypto.models.MasterKeyStorageData
+import ch.threema.localcrypto.models.Version1MasterKeyStorageData
 import io.mockk.every
 import io.mockk.mockk
 import io.mockk.verify
 import kotlin.test.Test
 import kotlin.test.assertEquals
+import kotlin.test.assertFails
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue
 
@@ -67,7 +71,7 @@ class MasterKeyStorageManagerTest {
 
     @Test
     fun `reading from existing version 1 key file`() {
-        val keyStorageDataMock = mockk<MasterKeyStorageData>()
+        val keyStorageDataMock = mockk<MasterKeyStorageData.Version1>()
         val keyStateMock = mockk<MasterKeyState>()
         val keyStorageManager = MasterKeyStorageManager(
             version2KeyFileManager = mockk {
@@ -87,9 +91,11 @@ class MasterKeyStorageManagerTest {
     }
 
     @Test
-    fun `writing key`() {
+    fun `writing key when version 1 key does not exist`() {
         val version2KeyFileManagerMock = mockk<Version2MasterKeyFileManager>(relaxed = true)
-        val version1KeyFileManagerMock = mockk<Version1MasterKeyFileManager>(relaxed = true)
+        val version1KeyFileManagerMock = mockk<Version1MasterKeyFileManager>(relaxed = true) {
+            every { keyFileExists() } returns false
+        }
         val keyStateMock = mockk<MasterKeyState>()
         val storageDataMock = mockk<MasterKeyStorageData.Version2>()
         val keyStorageManager = MasterKeyStorageManager(
@@ -105,4 +111,76 @@ class MasterKeyStorageManagerTest {
         verify(exactly = 1) { version2KeyFileManagerMock.writeKeyFile(storageDataMock) }
         verify(exactly = 1) { version1KeyFileManagerMock.deleteFile() }
     }
+
+    @Test
+    fun `writing key when version 1 key exists`() {
+        val version2KeyFileManagerMock = mockk<Version2MasterKeyFileManager>(relaxed = true)
+        val version1KeyFileManagerMock = mockk<Version1MasterKeyFileManager>(relaxed = true) {
+            every { keyFileExists() } returns true
+            every { readKeyFile() } returns MasterKeyStorageData.Version1(
+                data = Version1MasterKeyStorageData.Unprotected(
+                    masterKeyData = MasterKeyData(
+                        value = MASTER_KEY,
+                    ),
+                    verification = mockk(),
+                ),
+            )
+        }
+        val keyStateMock = mockk<MasterKeyState.Plain> {
+            every { masterKeyData } returns MasterKeyData(
+                value = MASTER_KEY.copyOf(),
+            )
+        }
+        val storageDataMock = mockk<MasterKeyStorageData.Version2>()
+        val keyStorageManager = MasterKeyStorageManager(
+            version2KeyFileManager = version2KeyFileManagerMock,
+            version1KeyFileManager = version1KeyFileManagerMock,
+            storageStateConverter = mockk {
+                every { toStorageData(keyStateMock as MasterKeyState) } returns storageDataMock
+            },
+        )
+
+        keyStorageManager.writeKey(keyStateMock)
+
+        verify(exactly = 1) { version2KeyFileManagerMock.writeKeyFile(storageDataMock) }
+        verify(exactly = 1) { version1KeyFileManagerMock.deleteFile() }
+    }
+
+    @Test
+    fun `writing key fails when version 1 key exists but has different value`() {
+        val version2KeyFileManagerMock = mockk<Version2MasterKeyFileManager>(relaxed = true)
+        val version1KeyFileManagerMock = mockk<Version1MasterKeyFileManager>(relaxed = true) {
+            every { keyFileExists() } returns true
+            every { readKeyFile() } returns MasterKeyStorageData.Version1(
+                data = Version1MasterKeyStorageData.Unprotected(
+                    masterKeyData = MasterKeyData(
+                        value = MASTER_KEY,
+                    ),
+                    verification = mockk(),
+                ),
+            )
+        }
+        val corruptedKey = MASTER_KEY.copyOf()
+        corruptedKey[0]++
+        val keyStateMock = mockk<MasterKeyState.Plain> {
+            every { masterKeyData } returns MasterKeyData(
+                value = corruptedKey,
+            )
+        }
+        val storageDataMock = mockk<MasterKeyStorageData.Version2>()
+        val keyStorageManager = MasterKeyStorageManager(
+            version2KeyFileManager = version2KeyFileManagerMock,
+            version1KeyFileManager = version1KeyFileManagerMock,
+            storageStateConverter = mockk {
+                every { toStorageData(keyStateMock as MasterKeyState) } returns storageDataMock
+            },
+        )
+
+        assertFails {
+            keyStorageManager.writeKey(keyStateMock)
+        }
+
+        verify(exactly = 0) { version2KeyFileManagerMock.writeKeyFile(storageDataMock) }
+        verify(exactly = 0) { version1KeyFileManagerMock.deleteFile() }
+    }
 }

+ 34 - 0
common/src/main/java/ch/threema/common/NoCloseOutputStream.kt

@@ -0,0 +1,34 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2025 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.common
+
+import java.io.FilterOutputStream
+import java.io.OutputStream
+
+/**
+ * Can be used to wrap around another [outputStream] in cases where we need to prevent the stream from being closed.
+ */
+class NoCloseOutputStream(outputStream: OutputStream) : FilterOutputStream(outputStream) {
+    override fun close() {
+        // do nothing here
+    }
+}

+ 1 - 1
gradle/libs.versions.toml

@@ -45,7 +45,7 @@ fastlaneScreengrab = "2.1.1"
 fastscroll = "1.3.0"
 firebaseMessaging = "24.1.0"
 gestureViews = "2.8.3"
-glide = "4.16.0" # Glide 4.15+ does not work on API 21
+glide = "5.0.5"
 hmsPush = "6.3.0.304"
 jacksonCore = "2.14.3" # Note: Any newer version breaks the compatibility with API versions older than 26. If we use a newer version of jackson core, the webclient does not work anymore on android 7.
 jna = "5.18.1" # Note: when updating this version, we need to include the libjnidispatch.so of the same version for each ABI in app/libs/. Note that the description in the readme files in app/libs/ files should be updated when this version is changed. The libjnidispatch.so files can be found in the jna.aar file: https://github.com/java-native-access/jna/tree/master/dist

+ 8 - 0
test-helpers/src/main/java/ch/threema/testhelpers/TestHelpers.kt

@@ -24,6 +24,7 @@ package ch.threema.testhelpers
 import app.cash.turbine.TurbineTestContext
 import ch.threema.common.DispatcherProvider
 import ch.threema.common.models.CryptographicByteArray
+import java.io.File
 import kotlin.random.Random
 import kotlin.test.assertEquals
 import kotlinx.coroutines.CoroutineDispatcher
@@ -95,3 +96,10 @@ fun TestScope.unconfinedTestDispatcherProvider(): DispatcherProvider {
             get() = testDispatcher
     }
 }
+
+fun createTempDirectory(prefix: String = "test"): File {
+    val directory = File.createTempFile(prefix, "test")
+    directory.delete()
+    directory.mkdirs()
+    return directory
+}