Browse Source

Version 5.1.1

Threema 2 years ago
parent
commit
0159a5af29
69 changed files with 826 additions and 458 deletions
  1. 5 5
      app/build.gradle
  2. 1 1
      app/src/main/java/ch/threema/app/ThreemaApplication.java
  3. 1 1
      app/src/main/java/ch/threema/app/activities/HomeActivity.java
  4. 3 1
      app/src/main/java/ch/threema/app/activities/MediaGalleryActivity.java
  5. 1 1
      app/src/main/java/ch/threema/app/activities/PermissionRequestActivity.kt
  6. 1 0
      app/src/main/java/ch/threema/app/activities/StorageManagementActivity.java
  7. 1 1
      app/src/main/java/ch/threema/app/activities/ThreemaAppCompatActivity.java
  8. 9 0
      app/src/main/java/ch/threema/app/activities/ballot/BallotWizardFragment1.java
  9. 4 4
      app/src/main/java/ch/threema/app/activities/wizard/WizardSafeRestoreActivity.java
  10. 19 1
      app/src/main/java/ch/threema/app/fragments/ComposeMessageFragment.java
  11. 4 0
      app/src/main/java/ch/threema/app/managers/ServiceManager.java
  12. 36 23
      app/src/main/java/ch/threema/app/processors/MessageProcessor.java
  13. 1 0
      app/src/main/java/ch/threema/app/services/AvatarCacheServiceImpl.java
  14. 1 0
      app/src/main/java/ch/threema/app/services/ContactService.java
  15. 20 6
      app/src/main/java/ch/threema/app/services/ContactServiceImpl.java
  16. 24 17
      app/src/main/java/ch/threema/app/services/MessageServiceImpl.java
  17. 43 13
      app/src/main/java/ch/threema/app/services/VoiceMessagePlayerService.kt
  18. 18 15
      app/src/main/java/ch/threema/app/services/messageplayer/AudioMessagePlayer.java
  19. 10 1
      app/src/main/java/ch/threema/app/ui/EmptyView.java
  20. 1 1
      app/src/main/java/ch/threema/app/ui/MentionSelectorPopup.java
  21. 6 2
      app/src/main/java/ch/threema/app/utils/ConfigUtils.java
  22. 5 0
      app/src/main/java/ch/threema/app/utils/DNDUtil.java
  23. 2 66
      app/src/main/java/ch/threema/app/utils/ForwardSecurityStatusSender.java
  24. 1 1
      app/src/main/java/ch/threema/app/utils/QuoteUtil.java
  25. 0 51
      app/src/main/java/ch/threema/app/utils/SMSUtil.java
  26. 2 2
      app/src/main/java/ch/threema/app/utils/SoundUtil.java
  27. 9 4
      app/src/main/java/ch/threema/app/voicemessage/VoiceRecorderActivity.java
  28. 9 0
      app/src/main/java/ch/threema/app/voip/services/VoipStateService.java
  29. 0 11
      app/src/main/res/drawable/bubble_compose_surface_variant.xml
  30. 119 0
      app/src/main/res/layout-land/activity_permission_request.xml
  31. 34 29
      app/src/main/res/layout/activity_permission_request.xml
  32. 8 8
      app/src/main/res/layout/activity_storagemanagement.xml
  33. 19 11
      app/src/main/res/layout/fragment_ballot_wizard1.xml
  34. 1 0
      app/src/main/res/layout/fragment_compose_message.xml
  35. 1 0
      app/src/main/res/layout/item_webclient_session_list.xml
  36. 1 2
      app/src/main/res/layout/popup_mention_selector.xml
  37. 10 0
      app/src/main/res/layout/view_empty.xml
  38. 1 0
      app/src/main/res/layout/view_permission_icon.xml
  39. 23 2
      app/src/main/res/values-be-rBY/strings.xml
  40. 1 1
      app/src/main/res/values-ca/strings.xml
  41. 75 69
      app/src/main/res/values-cs/strings.xml
  42. 4 4
      app/src/main/res/values-de/strings.xml
  43. 3 3
      app/src/main/res/values-es/strings.xml
  44. 2 2
      app/src/main/res/values-fr/strings.xml
  45. 1 1
      app/src/main/res/values-hu/strings.xml
  46. 2 2
      app/src/main/res/values-it/strings.xml
  47. 2 2
      app/src/main/res/values-ja/strings.xml
  48. 2 2
      app/src/main/res/values-night/colors.xml
  49. 2 2
      app/src/main/res/values-nl-rNL/strings.xml
  50. 2 2
      app/src/main/res/values-no/strings.xml
  51. 2 2
      app/src/main/res/values-pl/strings.xml
  52. 2 2
      app/src/main/res/values-pt-rBR/strings.xml
  53. 1 1
      app/src/main/res/values-rm/strings.xml
  54. 2 2
      app/src/main/res/values-ru/strings.xml
  55. 12 2
      app/src/main/res/values-sk/strings.xml
  56. 2 2
      app/src/main/res/values-tr/strings.xml
  57. 11 11
      app/src/main/res/values-uk/strings.xml
  58. 2 2
      app/src/main/res/values-zh-rCN/strings.xml
  59. 2 2
      app/src/main/res/values-zh-rTW/strings.xml
  60. 1 0
      app/src/main/res/values/dimens.xml
  61. 2 2
      app/src/main/res/values/strings.xml
  62. 6 3
      app/src/main/res/values/styles.xml
  63. 2 2
      app/src/onprem/res/values-de/strings.xml
  64. 2 2
      app/src/red/res/values-de/strings.xml
  65. 2 2
      app/src/store_google_work/res/values-de/strings.xml
  66. 13 6
      domain/src/main/java/ch/threema/domain/protocol/csp/connection/MessageProcessorInterface.java
  67. 28 16
      domain/src/main/java/ch/threema/domain/protocol/csp/connection/ThreemaConnection.java
  68. 115 18
      domain/src/main/java/ch/threema/domain/protocol/csp/fs/ForwardSecurityMessageProcessor.java
  69. 69 11
      domain/src/test/java/ch/threema/domain/protocol/csp/fs/ForwardSecurityMessageProcessorTest.java

+ 5 - 5
app/build.gradle

@@ -17,7 +17,7 @@ if (getGradle().getStartParameter().getTaskRequests().toString().contains("Hms")
 }
 
 // version codes
-def app_version = "5.1"
+def app_version = "5.1.1"
 def beta_suffix = "" // with leading dash
 
 /**
@@ -96,7 +96,7 @@ android {
         vectorDrawables.useSupportLibrary = true
         applicationId "ch.threema.app"
         testApplicationId 'ch.threema.app.test'
-        versionCode 906
+        versionCode 909
         versionName "${app_version}${beta_suffix}"
         resValue "string", "app_name", "Threema"
         // package name used for sync adapter - needs to match mime types below
@@ -714,9 +714,9 @@ dependencies {
     implementation "androidx.camera:camera-view:1.3.0-beta01"
     implementation 'androidx.camera:camera-video:1.3.0-beta01'
     implementation "androidx.media:media:1.6.0"
-    implementation 'androidx.media3:media3-exoplayer:1.0.2'
-    implementation 'androidx.media3:media3-ui:1.0.2'
-    implementation "androidx.media3:media3-session:1.0.2"
+    implementation 'androidx.media3:media3-exoplayer:1.1.1'
+    implementation 'androidx.media3:media3-ui:1.1.1'
+    implementation "androidx.media3:media3-session:1.1.1"
     implementation 'androidx.multidex:multidex:2.0.1'
     implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1"
     implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1"

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

@@ -232,7 +232,7 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 	public static final String INTENT_DATA_PIN = "ppin";
 	public static final String INTENT_DATA_HIDE_RECENTS = "hiderec";
 	public static final String INTENT_ACTION_FORWARD = "ch.threema.app.intent.FORWARD";
-	public static final String INTENT_ACTION_SHORTCUT_ADDED = "ch.threema.app.intent.SHORTCUT_ADDED";
+	public static final String INTENT_ACTION_SHORTCUT_ADDED = BuildConfig.APPLICATION_ID + ".intent.SHORTCUT_ADDED";
 
 	public static final String CONFIRM_TAG_CLOSE_BALLOT = "cb";
 

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

@@ -161,7 +161,7 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
 
 	private static final Logger logger = LoggingUtil.getThreemaLogger("HomeActivity");
 
-	private static final String THREEMA_CHANNEL_IDENTITY = "*THREEMA";
+	public static final String THREEMA_CHANNEL_IDENTITY = "*THREEMA";
 	private static final String THREEMA_CHANNEL_INFO_COMMAND = "Info";
 	private static final String THREEMA_CHANNEL_START_NEWS_COMMAND = "Start News";
 	private static final String THREEMA_CHANNEL_START_ANDROID_COMMAND = "Start Android";

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

@@ -304,15 +304,17 @@ public class MediaGalleryActivity extends ThreemaToolbarActivity implements
 		recyclerView.setLayoutManager(gridLayoutManager);
 		recyclerView.addItemDecoration(new MediaGridItemDecoration(getResources().getDimensionPixelSize(R.dimen.grid_spacing)));
 
-		EmptyView emptyView = new EmptyView(this);
+		final EmptyView emptyView = new EmptyView(this);
 		emptyView.setColorsInt(ConfigUtils.getColorFromAttribute(this, android.R.attr.colorBackground), ConfigUtils.getColorFromAttribute(this, R.attr.colorOnBackground));
 		emptyView.setup(getString(R.string.no_media_found_generic));
 		((ViewGroup) recyclerView.getParent()).addView(emptyView);
 		recyclerView.setEmptyView(emptyView);
+		emptyView.setLoading(true);
 		mediaGalleryAdapter = new MediaGalleryAdapter(this, this, messageReceiver, gridLayoutManager.getSpanCount());
 		recyclerView.setAdapter(mediaGalleryAdapter);
 
 		final Observer<List<AbstractMessageModel>> messageObserver = abstractMessageModels -> {
+			emptyView.setLoading(false);
 			mediaGalleryAdapter.setItems(abstractMessageModels);
 			if (actionMode != null) {
 				actionMode.invalidate();

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

@@ -274,7 +274,7 @@ class PermissionRequestActivity : ThreemaActivity() {
         }
 
         permissionSettingsExplanation.visibility =
-            visibleOrInvisible(shouldShowGoToSettingsExplanation(permissionState))
+            visibleOrGone(shouldShowGoToSettingsExplanation(permissionState))
 
         when {
             permissionState.granted -> {

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

@@ -135,6 +135,7 @@ public class StorageManagementActivity extends ThreemaToolbarActivity implements
 		progressBar = findViewById(R.id.progressbar);
 		selectedSpinnerItem = 0;
 		selectedMessageSpinnerItem = 0;
+		((TextView) findViewById(R.id.used_by_threema)).setText(getString(R.string.storage_threema, getString(R.string.app_name)));
 
 		if (deleteButton == null) {
 			logger.info("deleteButton is null");

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

@@ -72,7 +72,7 @@ public abstract class ThreemaAppCompatActivity extends AppCompatActivity {
 
 	@Override
 	public void onConfigurationChanged(@NonNull Configuration newConfig) {
-		int newDayNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
+		int newDayNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
 		if (savedDayNightMode != newDayNightMode) {
 			savedDayNightMode = newDayNightMode;
 			ConfigUtils.setCurrentDayNightMode(newDayNightMode == UI_MODE_NIGHT_YES ? MODE_NIGHT_YES : MODE_NIGHT_NO);

+ 9 - 0
app/src/main/java/ch/threema/app/activities/ballot/BallotWizardFragment1.java

@@ -36,6 +36,8 @@ import android.widget.ImageButton;
 import android.widget.TextView;
 
 import com.google.android.material.datepicker.MaterialDatePicker;
+import com.google.android.material.elevation.ElevationOverlayProvider;
+import com.google.android.material.textfield.TextInputLayout;
 import com.google.android.material.timepicker.MaterialTimePicker;
 
 import java.util.Calendar;
@@ -49,6 +51,7 @@ import androidx.recyclerview.widget.RecyclerView;
 import ch.threema.app.R;
 import ch.threema.app.adapters.ballot.BallotWizard1Adapter;
 import ch.threema.app.dialogs.FormatTextEntryDialog;
+import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.EditTextUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.storage.models.ballot.BallotChoiceModel;
@@ -136,6 +139,12 @@ public class BallotWizardFragment1 extends BallotWizardFragment implements Ballo
 		ItemTouchHelper itemTouchHelper = new ItemTouchHelper(swipeCallback);
 		itemTouchHelper.attachToRecyclerView(choiceRecyclerView);
 
+		// tint the edittext manually as TextInputLayout does not currently support elevation
+		ElevationOverlayProvider elevationOverlayProvider = new ElevationOverlayProvider(requireContext());
+		TextInputLayout textInputLayout = rootView.findViewById(R.id.textinputlayout_compose);
+		textInputLayout.setBoxBackgroundColor(elevationOverlayProvider.compositeOverlayIfNeeded(
+			ConfigUtils.getColorFromAttribute(requireContext(), R.attr.colorSurface),
+			getResources().getDimension(R.dimen.compose_edittext_elevation)));
 		this.createChoiceEditText = rootView.findViewById(R.id.create_choice_name);
 		this.createChoiceEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
 			@Override

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

@@ -23,7 +23,6 @@ package ch.threema.app.activities.wizard;
 
 import android.annotation.SuppressLint;
 import android.os.AsyncTask;
-import android.os.Build;
 import android.os.Bundle;
 import android.text.InputFilter;
 import android.text.InputType;
@@ -48,6 +47,7 @@ import ch.threema.app.threemasafe.ThreemaSafeMDMConfig;
 import ch.threema.app.threemasafe.ThreemaSafeServerInfo;
 import ch.threema.app.threemasafe.ThreemaSafeService;
 import ch.threema.app.threemasafe.ThreemaSafeServiceImpl;
+import ch.threema.app.ui.LongToast;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.DialogUtil;
 import ch.threema.app.utils.RuntimeUtil;
@@ -205,7 +205,7 @@ public class WizardSafeRestoreActivity extends WizardBackgroundActivity implemen
 		}
 
 		if (TestUtil.empty(password)) {
-			Toast.makeText(this, R.string.wrong_backupid_or_password_or_no_internet_connection, Toast.LENGTH_LONG).show();
+			LongToast.makeText(this, R.string.wrong_backupid_or_password_or_no_internet_connection, Toast.LENGTH_LONG).show();
 			return;
 		}
 
@@ -265,7 +265,7 @@ public class WizardSafeRestoreActivity extends WizardBackgroundActivity implemen
 							() -> {
 								// On fail
 								DialogUtil.dismissDialog(getSupportFragmentManager(), DIALOG_TAG_WORK_SYNC, true);
-								RuntimeUtil.runOnUiThread(() -> Toast.makeText(WizardSafeRestoreActivity.this, R.string.unable_to_fetch_configuration, Toast.LENGTH_LONG).show());
+								RuntimeUtil.runOnUiThread(() -> LongToast.makeText(WizardSafeRestoreActivity.this, R.string.unable_to_fetch_configuration, Toast.LENGTH_LONG).show());
 								logger.info("Unable to post work request for fetch2");
 								try {
 									userService.removeIdentity();
@@ -278,7 +278,7 @@ public class WizardSafeRestoreActivity extends WizardBackgroundActivity implemen
 						onSuccessfulRestore();
 					}
 				} else {
-					Toast.makeText(WizardSafeRestoreActivity.this, getString(R.string.safe_restore_failed) + ". " + failureMessage, Toast.LENGTH_LONG).show();
+					LongToast.makeText(WizardSafeRestoreActivity.this, getString(R.string.safe_restore_failed) + ". " + failureMessage, Toast.LENGTH_LONG).show();
 					if (safeMDMConfig.isRestoreForced()) {
 						finish();
 					}

+ 19 - 1
app/src/main/java/ch/threema/app/fragments/ComposeMessageFragment.java

@@ -23,6 +23,7 @@ package ch.threema.app.fragments;
 
 import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
 import static ch.threema.app.ThreemaApplication.getAppContext;
+import static ch.threema.app.activities.HomeActivity.THREEMA_CHANNEL_IDENTITY;
 import static ch.threema.app.adapters.ComposeMessageAdapter.MIN_CONSTRAINT_LENGTH;
 import static ch.threema.app.services.messageplayer.MessagePlayer.SOURCE_AUDIORECORDER;
 import static ch.threema.app.services.messageplayer.MessagePlayer.SOURCE_LIFECYCLE;
@@ -124,7 +125,9 @@ import com.google.android.material.badge.BadgeDrawable;
 import com.google.android.material.badge.BadgeUtils;
 import com.google.android.material.badge.ExperimentalBadgeUtils;
 import com.google.android.material.button.MaterialButton;
+import com.google.android.material.elevation.ElevationOverlayProvider;
 import com.google.android.material.progressindicator.CircularProgressIndicator;
+import com.google.android.material.textfield.TextInputLayout;
 import com.google.common.util.concurrent.ListenableFuture;
 
 import org.slf4j.Logger;
@@ -1111,6 +1114,13 @@ public class ComposeMessageFragment extends Fragment implements
 				scrollList(0);
 			});
 
+			// tint the edittext manually as TextInputLayout does not currently support elevation
+			ElevationOverlayProvider elevationOverlayProvider = new ElevationOverlayProvider(activity);
+			TextInputLayout textInputLayout = fragmentView.findViewById(R.id.textinputlayout_compose);
+			textInputLayout.setBoxBackgroundColor(elevationOverlayProvider.compositeOverlayIfNeeded(
+				ConfigUtils.getColorFromAttribute(activity, R.attr.colorSurface),
+				getResources().getDimension(R.dimen.compose_edittext_elevation)));
+
 			this.getValuesFromBundle(savedInstanceState);
 			this.handleIntent(activity.getIntent());
 			this.setupListeners();
@@ -2337,6 +2347,10 @@ public class ComposeMessageFragment extends Fragment implements
 			return false;
 		}
 
+		if (THREEMA_CHANNEL_IDENTITY.equals(contactModel.getIdentity())) {
+			return false;
+		}
+
 		if (composeMessageAdapter == null) {
 			return false;
 		}
@@ -3559,7 +3573,9 @@ public class ComposeMessageFragment extends Fragment implements
 				this.actionBarSubtitleTextView,
 				this.actionBarTitleTextView,
 				this.emojiMarkupUtil,
-				this.messageReceiver) || !requiredInstances()) {
+				this.messageReceiver,
+				isAdded(),
+				getActivity() != null) || !requiredInstances()) {
 			return;
 		}
 
@@ -4067,6 +4083,8 @@ public class ComposeMessageFragment extends Fragment implements
 			boolean hasDefaultRendering = false;
 
 			for (AbstractMessageModel message: selectedMessages) {
+				if (message == null) continue;
+
 				isQuotable = isQuotable && isQuotable(message);
 				showAsQRCode = showAsQRCode && canShowAsQRCode(message);
 				showAsText = showAsText && canShowAsText(message);

+ 4 - 0
app/src/main/java/ch/threema/app/managers/ServiceManager.java

@@ -310,6 +310,10 @@ public class ServiceManager {
 			this.connection.setMessageProcessor(this.getMessageProcessor());
 		}
 
+		if (this.connection.getForwardSecurityMessageProcessor() == null) {
+			this.connection.setForwardSecurityMessageProcessor(this.getForwardSecurityMessageProcessor());
+		}
+
 		//add message ACK processor
 		getMessageAckProcessor().setMessageService(this.getMessageService());
 		connection.addMessageAckListener(getMessageAckProcessor());

+ 36 - 23
app/src/main/java/ch/threema/app/processors/MessageProcessor.java

@@ -57,6 +57,8 @@ import ch.threema.domain.protocol.csp.coders.MessageBox;
 import ch.threema.domain.protocol.csp.coders.MessageCoder;
 import ch.threema.domain.protocol.csp.connection.MessageProcessorInterface;
 import ch.threema.domain.protocol.csp.fs.ForwardSecurityMessageProcessor;
+import ch.threema.domain.protocol.csp.fs.ForwardSecurityMessageProcessor.ForwardSecurityDecryptionResult;
+import ch.threema.domain.protocol.csp.fs.ForwardSecurityMessageProcessor.PeerRatchetIdentifier;
 import ch.threema.domain.protocol.csp.messages.AbstractGroupMessage;
 import ch.threema.domain.protocol.csp.messages.AbstractMessage;
 import ch.threema.domain.protocol.csp.messages.BadMessageException;
@@ -178,42 +180,53 @@ public class MessageProcessor implements MessageProcessorInterface {
 				if (this.blackListService != null && this.blackListService.has(msg.getFromIdentity())) {
 					logger.debug("Direct message from {}: Contact blacklisted. Ignoring", msg.getFromIdentity());
 					//ignore message of blacklisted member
-					return ProcessIncomingResult.processed(msg.getType());
+					return ProcessIncomingResult.processed(msg.getType(), null);
 				}
 			}
 
 			this.contactService.setActive(msg.getFromIdentity());
 
+			PeerRatchetIdentifier peerRatchet = null;
+
 			if (msg instanceof ForwardSecurityEnvelopeMessage) {
 				if (!ConfigUtils.isForwardSecurityEnabled()) {
 					logger.debug("PFS is disabled in build");
-					return ProcessIncomingResult.processed(msg.getType());
+					return ProcessIncomingResult.processed(msg.getType(), null);
 				}
 
 				// Decapsulate PFS message
 				final Contact contact = this.contactService.getByIdentity(msg.getFromIdentity());
 				if (contact == null) {
 					logger.debug("Ignoring FS message from unknown identity {}", msg.getFromIdentity());
-					return ProcessIncomingResult.processed();
+					return ProcessIncomingResult.processed(null);
 				}
-				AbstractMessage decapMessage = forwardSecurityMessageProcessor.processEnvelopeMessage(contact, (ForwardSecurityEnvelopeMessage) msg);
-				if (decapMessage != null) {
+				ForwardSecurityDecryptionResult result = forwardSecurityMessageProcessor.processEnvelopeMessage(contact, (ForwardSecurityEnvelopeMessage) msg);
+				peerRatchet = result.peerRatchetIdentifier;
+				if (result.message != null) {
 					// Replace current abstract message with decapsulated version
-					msg = decapMessage;
+					msg = result.message;
 				} else {
 					// Control message processed; nothing left to do
-					return ProcessIncomingResult.processed();
+					return ProcessIncomingResult.processed(null, peerRatchet);
 				}
+
+				logger.info(
+					"Processing decrypted message {} from {} to {} (type {})",
+					msg.getMessageId(),
+					msg.getFromIdentity(),
+					msg.getToIdentity(),
+					Utils.byteToHex((byte) msg.getType(), true, true)
+				);
 			} else {
 				forwardSecurityMessageProcessor.warnIfMessageWithoutForwardSecurityReceived(msg);
 			}
 
 			if (msg instanceof TypingIndicatorMessage) {
-				return processTypingIndicatorMessage((TypingIndicatorMessage) msg);
+				return processTypingIndicatorMessage((TypingIndicatorMessage) msg, peerRatchet);
 			} else if (msg instanceof DeliveryReceiptMessage) {
-				return processDeliveryReceiptMessage((DeliveryReceiptMessage)msg);
+				return processDeliveryReceiptMessage((DeliveryReceiptMessage)msg, peerRatchet);
 			} else if (msg instanceof GroupDeliveryReceiptMessage) {
-				return processGroupDeliveryReceiptMessage((GroupDeliveryReceiptMessage)msg);
+				return processGroupDeliveryReceiptMessage((GroupDeliveryReceiptMessage)msg, peerRatchet);
 			}
 
 			/* send delivery receipt (but not for non-queued messages or delivery receipts) */
@@ -229,7 +242,7 @@ public class MessageProcessor implements MessageProcessorInterface {
 					)
 				) {
 					logger.info("Message {} discarded - from hidden contact with block unknown enabled", boxmsg.getMessageId());
-					return ProcessIncomingResult.processed(msg.getType());
+					return ProcessIncomingResult.processed(msg.getType(), peerRatchet);
 				}
 
 				switch(this.processAbstractMessage(msg)) {
@@ -239,21 +252,21 @@ public class MessageProcessor implements MessageProcessorInterface {
 					case FAILED:
 						return ProcessIncomingResult.failed();
 					case IGNORED:
-						return ProcessIncomingResult.processed(msg.getType());
+						return ProcessIncomingResult.processed(msg.getType(), peerRatchet);
 				}
 			}
 
-			return ProcessIncomingResult.processed(msg.getType());
+			return ProcessIncomingResult.processed(msg.getType(), peerRatchet);
 
 		} catch (MissingPublicKeyException e) {
 			if(this.preferenceService.isBlockUnknown()) {
 				//its ok, return true and save nothing
-				return ProcessIncomingResult.processed();
+				return ProcessIncomingResult.processed(null);
 			}
 
 			if(this.blackListService != null && this.blackListService.has(boxmsg.getFromIdentity())) {
 				//its ok, a black listed identity, save NOTHING
-				return ProcessIncomingResult.processed();
+				return ProcessIncomingResult.processed(null);
 			}
 
 			logger.error("Missing public key", e);
@@ -262,7 +275,7 @@ public class MessageProcessor implements MessageProcessorInterface {
 		} catch (BadMessageException e) {
 			logger.error("Bad message", e);
 			logger.warn("Message {} error: invalid - dropping msg.", boxmsg.getMessageId());
-			return ProcessIncomingResult.processed();
+			return ProcessIncomingResult.processed(null);
 
 		} catch (Exception e) {
 			logger.error("Unknown exception while processing BoxedMessage", e);
@@ -270,16 +283,16 @@ public class MessageProcessor implements MessageProcessorInterface {
 		}
 	}
 
-	private ProcessIncomingResult processTypingIndicatorMessage(@NonNull TypingIndicatorMessage msg) {
+	private ProcessIncomingResult processTypingIndicatorMessage(@NonNull TypingIndicatorMessage msg, @Nullable PeerRatchetIdentifier peerRatchet) {
 		if (this.contactService.getByIdentity(msg.getFromIdentity()) != null) {
 			this.contactService.setIsTyping(msg.getFromIdentity(), msg.isTyping());
 		} else {
 			logger.debug("Ignoring typing indicator message from unknown identity {}", msg.getFromIdentity());
 		}
-		return ProcessIncomingResult.processed(msg.getType());
+		return ProcessIncomingResult.processed(msg.getType(), peerRatchet);
 	}
 
-	private ProcessIncomingResult processDeliveryReceiptMessage(@NonNull DeliveryReceiptMessage msg) {
+	private ProcessIncomingResult processDeliveryReceiptMessage(@NonNull DeliveryReceiptMessage msg, @Nullable PeerRatchetIdentifier peerRatchet) {
 		final @Nullable MessageState state;
 		switch (msg.getReceiptType()) {
 			case ProtocolDefines.DELIVERYRECEIPT_MSGRECEIVED:
@@ -309,10 +322,10 @@ public class MessageProcessor implements MessageProcessorInterface {
 		} else {
 			logger.warn("Message {} error: unknown delivery receipt type", msg.getMessageId());
 		}
-		return ProcessIncomingResult.processed(msg.getType());
+		return ProcessIncomingResult.processed(msg.getType(), peerRatchet);
 	}
 
-	private ProcessIncomingResult processGroupDeliveryReceiptMessage(@NonNull GroupDeliveryReceiptMessage msg) {
+	private ProcessIncomingResult processGroupDeliveryReceiptMessage(@NonNull GroupDeliveryReceiptMessage msg, @Nullable PeerRatchetIdentifier peerRatchet) {
 		final @Nullable MessageState state;
 		switch (msg.getReceiptType()) {
 			case ProtocolDefines.DELIVERYRECEIPT_MSGUSERACK:
@@ -328,7 +341,7 @@ public class MessageProcessor implements MessageProcessorInterface {
 		if (state != null) {
 			if (groupService.runCommonGroupReceiveSteps(msg) != SUCCESS) {
 				// If the common group receive steps did not succeed, ignore this delivery receipt
-				return ProcessIncomingResult.processed(msg.getType());
+				return ProcessIncomingResult.processed(msg.getType(), peerRatchet);
 			}
 			for (MessageId msgId : msg.getReceiptMessageIds()) {
 				logger.info("Message {}: group delivery receipt for {} (state = {})", msg.getMessageId(), msgId, state);
@@ -337,7 +350,7 @@ public class MessageProcessor implements MessageProcessorInterface {
 		} else {
 			logger.warn("Message {} error: unknown or unsupported delivery receipt type", msg.getMessageId());
 		}
-		return ProcessIncomingResult.processed(msg.getType());
+		return ProcessIncomingResult.processed(msg.getType(), peerRatchet);
 	}
 
 	private boolean processBallotVoteInterface(BallotVoteInterface msg) throws NotAllowedException {

+ 1 - 0
app/src/main/java/ch/threema/app/services/AvatarCacheServiceImpl.java

@@ -197,6 +197,7 @@ final public class AvatarCacheServiceImpl implements AvatarCacheService {
 			}
 			return requestBuilder.submit().get();
 		} catch (ExecutionException | InterruptedException e) {
+			logger.error("Error while getting avatar bitmap", e);
 			Thread.currentThread().interrupt();
 			return null;
 		}

+ 1 - 0
app/src/main/java/ch/threema/app/services/ContactService.java

@@ -378,6 +378,7 @@ public interface ContactService extends AvatarService<ContactModel> {
 	 * picture set, the blob ID is {@link ContactModel#NO_PROFILE_PICTURE_BLOB_ID}.
 	 */
 	@NonNull
+	@WorkerThread
 	ProfilePictureUploadData getUpdatedProfilePictureUploadData();
 	boolean updateProfilePicture(ContactSetProfilePictureMessage msg);
 	boolean deleteProfilePicture(ContactDeleteProfilePictureMessage msg);

+ 20 - 6
app/src/main/java/ch/threema/app/services/ContactServiceImpl.java

@@ -1169,12 +1169,21 @@ public class ContactServiceImpl implements ContactService {
 	}
 
 	@Override
+	@WorkerThread
 	@NonNull
 	public ProfilePictureUploadData getUpdatedProfilePictureUploadData() {
-		Bitmap contactPhoto = getMyProfilePicture();
+		Bitmap contactPhoto;
+		try {
+			contactPhoto = getMyProfilePicture();
+		} catch (ThreemaException e) {
+			logger.error("Could not get my profile picture", e);
+			// Returning empty profile picture upload data means no set or delete profile picture
+			// message will be sent.
+			return new ProfilePictureUploadData();
+		}
 		if (contactPhoto == null) {
-			// If there is no profile picture set, then return empty upload data with an empty
-			// byte array as blob ID.
+			// If there is no profile picture set, then return empty upload data with an empty byte
+			// array as blob ID. This means, that a delete-profile-picture message will be sent.
 			ProfilePictureUploadData data = new ProfilePictureUploadData();
 			data.blobId = ContactModel.NO_PROFILE_PICTURE_BLOB_ID;
 			return data;
@@ -1207,10 +1216,15 @@ public class ContactServiceImpl implements ContactService {
 		}
 	}
 
+	@WorkerThread
 	@Nullable
-	private Bitmap getMyProfilePicture() {
-		ContactModel myContactModel = getByIdentity(getMe().getIdentity());
-		return getAvatar(myContactModel, true, false);
+	private Bitmap getMyProfilePicture() throws ThreemaException {
+		ContactModel myContactModel = getMe();
+		Bitmap myProfilePicture = getAvatar(myContactModel, true, false);
+		if (myProfilePicture == null && fileService.hasContactAvatarFile(myContactModel)) {
+			throw new ThreemaException("Could not load profile picture despite having set one");
+		}
+		return myProfilePicture;
 	}
 
 	@Nullable

+ 24 - 17
app/src/main/java/ch/threema/app/services/MessageServiceImpl.java

@@ -561,9 +561,12 @@ public class MessageServiceImpl implements MessageService {
 		return messageModel;
 	}
 
+	@WorkerThread
 	@Override
 	public void executeProfilePictureDistribution(@NonNull AbstractMessage message) {
-		// Step 1: abort if message does not allow user profile distribution
+		final String prefix = "Profile picture distribution";
+
+		// Step 1: Abort if message does not allow user profile distribution
 		if (!message.allowUserProfileDistribution()) {
 			return;
 		}
@@ -572,13 +575,13 @@ public class MessageServiceImpl implements MessageService {
 
 		ContactModel contactModel = contactService.getByIdentity(toIdentity);
 		if (contactModel == null) {
-			logger.warn("Cannot send profile picture: ");
+			logger.warn("{}: Contact model not found", prefix);
 			return;
 		}
 
-		// Step 2: abort if the contact's id is ECHOECHO or a Gateway ID
+		// Step 2: Abort if the contact's id is ECHOECHO or a Gateway ID
 		if (ContactUtil.isEchoEchoOrChannelContact(contactModel)) {
-			logger.info("Contact {} should not receive the profile picture", toIdentity);
+			logger.info("{}: Contact {} should not receive the profile picture", prefix, toIdentity);
 			return;
 		}
 
@@ -591,43 +594,47 @@ public class MessageServiceImpl implements MessageService {
 			}
 		}
 
-		// Step 3: abort the contact should not receive the profile picture according to settings
+		// Step 3: Abort if the contact should not receive the profile picture according to settings
 		if (!contactService.isContactAllowedToReceiveProfilePicture(contactModel)) {
-			logger.info("Contact {} should not receive the profile picture", toIdentity);
+			logger.info("{}: Contact {} is not allowed to receive the profile picture", prefix, toIdentity);
 			return;
 		}
 
-		// Step 4: upload profile picture to blob server if no valid cached blob id exists
+		// Step 4: Upload profile picture to blob server if no valid cached blob id exists
 		ContactService.ProfilePictureUploadData data = contactService.getUpdatedProfilePictureUploadData();
 		if (data.blobId == null) {
-			logger.warn("Blob ID is null; abort profile picture distribution");
+			logger.warn("{}: Blob ID is null; abort", prefix);
 			return;
 		}
 
 		// Step 5: If the currently cached blob ID equals the blob ID that was most recently
 		// distributed to the contact, abort these steps
 		if (Arrays.equals(data.blobId, contactModel.getProfilePicBlobID())) {
-			logger.debug("Contact {} already has the latest profile picture", toIdentity);
+			logger.debug("{}: Contact {} already has the latest profile picture", prefix, toIdentity);
 			return;
 		}
 
 		// Step 6 and 7: Send a set-profile-picture message to the contact using the cached blob ID
 		// and store the blob ID as the most recently used blob ID for this contact
 		if (data.blobId != ContactModel.NO_PROFILE_PICTURE_BLOB_ID) {
-			if (!sendContactSetProfilePictureMessage(data, contactModel)) {
-				logger.warn("Could not enqueue set profile picture message");
-				return;
+			if (sendContactSetProfilePictureMessage(data, contactModel)) {
+				logger.info("{}: Profile picture successfully enqueued for {}", prefix, toIdentity);
+			} else {
+				logger.warn("{}: Could not enqueue set profile picture message", prefix);
 			}
 		} else {
-			if (!sendContactDeleteProfilePictureMessage(contactModel)) {
-				logger.warn("Could not enqueue delete profile picture message");
-				return;
+			if (sendContactDeleteProfilePictureMessage(contactModel)) {
+				logger.info("{}: Profile picture deletion successfully enqueued for {}", prefix, toIdentity);
+			} else {
+				logger.warn("{}: Could not enqueue delete profile picture message", prefix);
 			}
 		}
-
-		logger.info("Profile picture successfully sent to {}", toIdentity);
 	}
 
+	/**
+	 * Send a set-profile-picture or delete-profile-picture message to the specified contact.
+	 */
+	@WorkerThread
 	@Override
 	public boolean sendProfilePicture(@NonNull ContactModel contactModel) {
 		ContactService.ProfilePictureUploadData data = contactService.getUpdatedProfilePictureUploadData();

+ 43 - 13
app/src/main/java/ch/threema/app/services/VoiceMessagePlayerService.kt

@@ -27,8 +27,10 @@ import android.app.NotificationChannel
 import android.app.NotificationManager
 import android.app.PendingIntent
 import android.app.PendingIntent.*
+import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
+import android.content.IntentFilter
 import android.media.AudioManager
 import android.media.AudioManager.OnAudioFocusChangeListener
 import android.net.Uri
@@ -39,8 +41,8 @@ import androidx.core.app.NotificationCompat
 import androidx.core.app.NotificationManagerCompat
 import androidx.core.content.res.ResourcesCompat
 import androidx.media3.common.*
-import androidx.media3.exoplayer.DefaultRenderersFactory
 import androidx.media3.exoplayer.DefaultLoadControl
+import androidx.media3.exoplayer.DefaultRenderersFactory
 import androidx.media3.exoplayer.ExoPlayer
 import androidx.media3.exoplayer.audio.AudioSink
 import androidx.media3.session.*
@@ -59,6 +61,14 @@ import com.google.common.util.concurrent.ListenableFuture
 @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
 class VoiceMessagePlayerService : MediaSessionService(), SensorListener, OnAudioFocusChangeListener {
     private val logger = LoggingUtil.getThreemaLogger(TAG)
+    private val audioBecomingNoisyFilter = IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
+    private val audioBecomingNoisyReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context, intent: Intent) {
+            if (intent.action == AudioManager.ACTION_AUDIO_BECOMING_NOISY) {
+                player.pause()
+            }
+        }
+    }
 
     private lateinit var player: ExoPlayer
     private lateinit var mediaSession: MediaSession
@@ -97,6 +107,7 @@ class VoiceMessagePlayerService : MediaSessionService(), SensorListener, OnAudio
 
     override fun onUpdateNotification(session: MediaSession, startInForegroundRequired: Boolean) {
         if (player.playbackState == Player.STATE_ENDED && !startInForegroundRequired) {
+            logger.info("Playback ended. Dismissing service.")
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                 stopForeground(STOP_FOREGROUND_REMOVE)
             } else {
@@ -143,20 +154,24 @@ class VoiceMessagePlayerService : MediaSessionService(), SensorListener, OnAudio
                 .build()
 
         preferenceService?.let {
-            if (it.isUseProximitySensor) {
-                player.addListener(object : Player.Listener {
-                    override fun onIsPlayingChanged(isPlaying: Boolean) {
-                        logger.debug("onIsPlayingChanged {}", isPlaying)
-                        if (isPlaying) {
+            player.addListener(object : Player.Listener {
+                override fun onIsPlayingChanged(isPlaying: Boolean) {
+                    logger.debug("onIsPlayingChanged {}", isPlaying)
+                    if (isPlaying) {
+                        logger.info("Start playing")
+                        if (it.isUseProximitySensor) {
                             sensorService?.registerSensors(TAG, this@VoiceMessagePlayerService)
-                            requestAudioFocus()
-                        } else {
+                        }
+                        requestAudioFocus()
+                    } else {
+                        logger.info("Stop playing")
+                        if (it.isUseProximitySensor) {
                             sensorService?.unregisterSensors(TAG)
-                            releaseAudioFocus()
                         }
+                        releaseAudioFocus()
                     }
-                })
-            }
+                }
+            })
         }
 
         val mediaSessionCallback = (object : Callback {
@@ -233,7 +248,11 @@ class VoiceMessagePlayerService : MediaSessionService(), SensorListener, OnAudio
                         getString(R.string.vm_fg_service_not_allowed),
                         NotificationManager.IMPORTANCE_DEFAULT
                 )
-        notificationManagerCompat.createNotificationChannel(channel)
+        try {
+            notificationManagerCompat.createNotificationChannel(channel)
+        } catch (e: Exception) {
+            logger.error("Unable to create notification channel.", e)
+        }
     }
 
     override fun onSensorChanged(key: String?, value: Boolean) {
@@ -251,14 +270,25 @@ class VoiceMessagePlayerService : MediaSessionService(), SensorListener, OnAudio
             audioManager.requestAudioFocus(
                 this,
                 AudioManager.STREAM_MUSIC,
-                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
+                // we intentionally use a transient audio focus here, as the use expects playback of
+                // previous audio (e.g. music playback) to continue after listening to the voice message.
+                // ducking allows notification sounds to go through.
+                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
             )
             hasAudioFocus = true
+            registerReceiver(audioBecomingNoisyReceiver, audioBecomingNoisyFilter)
         }
     }
 
     private fun releaseAudioFocus() {
         audioManager.abandonAudioFocus(this)
+        if (hasAudioFocus) {
+            try {
+                unregisterReceiver(audioBecomingNoisyReceiver)
+            } catch (e: IllegalArgumentException) {
+                // not registered... ignore exceptions
+            }
+        }
         hasAudioFocus = false
     }
 

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

@@ -101,7 +101,7 @@ public class AudioMessagePlayer extends MessagePlayer {
 	private final Player.Listener playerListener = new Player.Listener() {
 			@Override
 			public void onIsLoadingChanged(boolean isLoading) {
-				logger.debug(isLoading ? "@ onLoading" : "@ onLoaded");
+				logger.info(isLoading ? "onLoading" : "onLoaded");
 			}
 
 			@Override
@@ -109,10 +109,10 @@ public class AudioMessagePlayer extends MessagePlayer {
 				MediaController mediaController = getMediaController();
 				if (mediaController != null) {
 					if (isPlaying) {
-						logger.debug("@ onPlay");
+						logger.info("onPlay");
 						makeResume(SOURCE_UI_TOGGLE);
 					} else if (mediaController.getPlaybackState() != Player.STATE_ENDED && playerMediaMatchesControllerMedia()) {
-						logger.debug("@ onPause");
+						logger.info("onPause");
 						makePause(SOURCE_UI_TOGGLE);
 					}
 				}
@@ -121,11 +121,11 @@ public class AudioMessagePlayer extends MessagePlayer {
 		@Override
 		public void onPlaybackStateChanged(int playbackState) {
 			if (playbackState == Player.STATE_ENDED) {
-				logger.debug("@ onStopped");
+				logger.info("onStopped");
 				AudioMessagePlayer.super.stop();
 				ListenerManager.messagePlayerListener.handle(listener -> listener.onAudioPlayEnded(getMessageModel()));
 			} else if (playbackState == Player.STATE_READY) {
-				logger.debug("@ onReady");
+				logger.info("onReady");
 				markAsConsumed();
 				prepared();
 			}
@@ -134,7 +134,7 @@ public class AudioMessagePlayer extends MessagePlayer {
 		@Override
 		public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, int reason) {
 			if (reason == Player.DISCONTINUITY_REASON_SEEK) {
-				logger.debug("@ onSeekEnded {} {} {}", reason, oldPosition.positionMs, newPosition.positionMs);
+				logger.info("onSeekEnded {} {} {}", reason, oldPosition.positionMs, newPosition.positionMs);
 
 				// seek ended
 				if (oldPosition != newPosition) {
@@ -172,11 +172,10 @@ public class AudioMessagePlayer extends MessagePlayer {
 		this.position = 0;
 		this.duration = 0;
 
-		logger.debug("open uri = {}", decryptedFileUri);
+		logger.info("Open voice message file {}", decryptedFileUri);
 
 		MediaController mediaController = getMediaController();
 		if (mediaController != null) {
-
 			String displayName;
 			Bitmap artworkBitmap = null;
 			if (!this.preferenceService.isShowMessagePreview() || this.hiddenChatsListService.has(currentMessageReceiver.getUniqueIdString())) {
@@ -214,6 +213,10 @@ public class AudioMessagePlayer extends MessagePlayer {
 			mediaController.setPlayWhenReady(false);
 			mediaController.addListener(playerListener);
 			mediaController.prepare();
+
+			logger.info("MediaController prepared");
+		} else {
+			logger.info("Unable to get MediaController");
 		}
 	}
 
@@ -221,7 +224,7 @@ public class AudioMessagePlayer extends MessagePlayer {
 	 * called after the media player was prepared
 	 */
 	private void prepared() {
-		logger.debug("prepared");
+		logger.info("Media Player is prepared");
 
 		MediaController mediaController = getMediaController();
 		if (mediaController == null) {
@@ -230,7 +233,7 @@ public class AudioMessagePlayer extends MessagePlayer {
 
 		if (!playerMediaMatchesControllerMedia()) {
 			// another media player
-			logger.debug("another player instance");
+			logger.info("Player media does not match controller media");
 			return;
 		}
 
@@ -244,7 +247,7 @@ public class AudioMessagePlayer extends MessagePlayer {
 				duration = (int) (((FileDataModel) d).getDurationSeconds() * SECOND_IN_MILLIS);
 			}
 		}
-		logger.debug("duration = {}", duration);
+		logger.info("Duration = {}", duration);
 
 		if (this.position > mediaController.getCurrentPosition()) {
 			mediaController.seekTo(this.position);
@@ -254,7 +257,7 @@ public class AudioMessagePlayer extends MessagePlayer {
 	}
 
 	private void onSeekCompleted() {
-		logger.debug("play from position {}", this.position);
+		logger.info("Seek completed. Play from position {}", this.position);
 
 		MediaController mediaController = getMediaController();
 		if (mediaController != null) {
@@ -333,7 +336,7 @@ public class AudioMessagePlayer extends MessagePlayer {
 
 	@Override
 	protected void play(final boolean autoPlay) {
-		logger.debug("play");
+		logger.info("Play button pressed");
 		if (this.state == State_PAUSE) {
 			MediaController mediaController = getMediaController();
 			if (mediaController != null) {
@@ -361,7 +364,7 @@ public class AudioMessagePlayer extends MessagePlayer {
 	}
 
 	private void releasePlayer() {
-		logger.debug("releasePlayer");
+		logger.info("Release Player");
 
 		if (mediaPositionListener != null) {
 			logger.debug("mediaPositionListener.interrupt()");
@@ -372,7 +375,7 @@ public class AudioMessagePlayer extends MessagePlayer {
 		MediaController mediaController = getMediaController();
 		if (mediaController != null) {
 			if (playerMediaMatchesControllerMedia()) {
-				logger.debug("mediaController stopped and cleared");
+				logger.info("MediaController stopped and cleared");
 				mediaController.stop();
 				mediaController.clearMediaItems();
 				this.position = 0;

+ 10 - 1
app/src/main/java/ch/threema/app/ui/EmptyView.java

@@ -35,10 +35,13 @@ import android.widget.TextView;
 import androidx.annotation.ColorInt;
 import androidx.annotation.ColorRes;
 
+import com.google.android.material.progressindicator.CircularProgressIndicator;
+
 import ch.threema.app.R;
 
 public class EmptyView extends LinearLayout {
 	private TextView emptyText;
+	private CircularProgressIndicator loadingView;
 
 	public EmptyView(Context context) {
 		this(context, null, 0);
@@ -65,7 +68,8 @@ public class EmptyView extends LinearLayout {
 		LayoutInflater.from(context).inflate(R.layout.view_empty, this, true);
 		setVisibility(View.GONE);
 
-		this.emptyText = (TextView) getChildAt(0);
+		this.loadingView = (CircularProgressIndicator) getChildAt(0);
+		this.emptyText = (TextView) getChildAt(1);
 	}
 
 	public void setup(int label) {
@@ -85,4 +89,9 @@ public class EmptyView extends LinearLayout {
 		this.setBackgroundColor(background);
 		this.emptyText.setTextColor(foreground);
 	}
+
+	public void setLoading(boolean isLoading) {
+		this.loadingView.setVisibility(isLoading ? VISIBLE : GONE);
+		this.emptyText.setVisibility(isLoading ? GONE : VISIBLE);
+	}
 }

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

@@ -77,7 +77,7 @@ public class MentionSelectorPopup extends PopupWindow implements MentionSelector
 	private final ContactModel allContactModel;
 	private final MentionSelectorListener mentionSelectorListener;
 	private ComposeEditText editText;
-	private int dividersHeight, viewableSpaceHeight;
+	private int viewableSpaceHeight;
 	private final TextWatcher textWatcher = new TextWatcher() {
 		private void run() {
 			dismiss();

+ 6 - 2
app/src/main/java/ch/threema/app/utils/ConfigUtils.java

@@ -239,7 +239,7 @@ public class ConfigUtils {
 
 	/* device creates distorted audio recordings with a 44.1kHz sampling rate */
 	public static boolean hasBrokenAudioRecorder() {
-		return ConfigUtils.isXiaomiDevice() && Build.MODEL.startsWith("Mi 9T");
+		return ConfigUtils.isXiaomiDevice() && "Mi 9T".equals(Build.MODEL);
 	}
 
 	public static boolean hasScopedStorage() {
@@ -334,8 +334,12 @@ public class ConfigUtils {
 						miuiVersion = 10;
 					} else if (version.startsWith("V11")) {
 						miuiVersion = 11;
-					} else if (version.startsWith("V12") || version.startsWith("V13")) {
+					} else if (version.startsWith("V12")) {
 						miuiVersion = 12;
+					} else if (version.startsWith("V13")) {
+						miuiVersion = 13;
+					} else if  (version.startsWith("V14")) {
+						miuiVersion = 14;
 					}
 				}
 			} catch (Exception ignored) { }

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

@@ -277,11 +277,13 @@ public class DNDUtil {
 					NotificationChannelGroupCompat notificationChannelGroupCompat = notificationManagerCompat.getNotificationChannelGroupCompat(groupName);
 					if (notificationChannelGroupCompat != null) {
 						if (notificationChannelGroupCompat.isBlocked()) {
+							logger.info("Notification channel group is blocked");
 							return true;
 						}
 					}
 				}
 				canBypassDND = notificationChannelCompat.canBypassDnd();
+				logger.info("Notification channel can bypass DND = {}", canBypassDND);
 			}
 		}
 
@@ -290,10 +292,13 @@ public class DNDUtil {
 				/* we do not play a ringtone sound if system-wide DND is enabled - except for starred contacts */
 				switch (notificationManager.getCurrentInterruptionFilter()) {
 					case NotificationManager.INTERRUPTION_FILTER_NONE:
+						logger.info("Interruption filter set to NONE");
 						isSystemMuted = true;
 						break;
 					case NotificationManager.INTERRUPTION_FILTER_PRIORITY:
+						logger.info("Interruption filter set to PRIORITY");
 						isSystemMuted = !isStarredContact(messageReceiver);
+						logger.info("Contact is starred = {}", !isSystemMuted);
 						break;
 					default:
 						break;

+ 2 - 66
app/src/main/java/ch/threema/app/utils/ForwardSecurityStatusSender.java

@@ -146,24 +146,12 @@ public class ForwardSecurityStatusSender implements ForwardSecurityStatusListene
 
 	@Override
 	public void messagesSkipped(@Nullable DHSessionId sessionId, @NonNull Contact contact, int numSkipped) {
-		postStatusMessageDebug(String.format("Skipped %s messages (session-id=%s)", numSkipped, sessionId), contact);
+		logger.info("Skipped {} messages from contact {} (session-id={})", numSkipped, contact.getIdentity(), sessionId);
 	}
 
 	@Override
 	public void messageOutOfOrder(@Nullable DHSessionId sessionId, @NonNull Contact contact, @Nullable MessageId messageId) {
-		postStatusMessageDebug(String.format("Message out of order (session-id=%s, message-id=%s)", sessionId, messageId), contact);
-
-		if (messageId != null && hasLastMessageId(contact, messageId)) {
-			// If the latest message of a contact is processed again, it cannot be decrypted again due to FS. It is very
-			// likely that the message has been processed but could not be acknowledged on the server. Therefore we do
-			// not show a warning if the message is already displayed in the chat.
-			logger.warn("The latest message with id '{}' was processed twice. Ignoring the second message.", messageId);
-			if (debug) {
-				postStatusMessageDebug(String.format("The latest message with was processed twice (message-id=%s)", messageId), contact);
-			}
-		} else {
-			postStatusMessage(contact, ForwardSecurityStatusType.FORWARD_SECURITY_MESSAGE_OUT_OF_ORDER);
-		}
+		postStatusMessageDebug(String.format("Message out of order (session-id=%s, message-id=%s). Please report this to the Android Team!", sessionId, messageId), contact);
 	}
 
 	@Override
@@ -295,56 +283,4 @@ public class ForwardSecurityStatusSender implements ForwardSecurityStatusListene
 		}
 	}
 
-	private boolean hasLastMessageId(@NonNull Contact contact, @NonNull MessageId messageId) {
-		ContactMessageReceiver r = contactService.createReceiver(contactService.getByIdentity(contact.getIdentity()));
-
-		List<AbstractMessageModel> messageModels = this.messageService.getMessagesForReceiver(r, new MessageService.MessageFilter() {
-			@Override
-			public long getPageSize() {
-				return 1;
-			}
-
-			@Override
-			public Integer getPageReferenceId() {
-				return null;
-			}
-
-			@Override
-			public boolean withStatusMessages() {
-				return false;
-			}
-
-			@Override
-			public boolean withUnsaved() {
-				return false;
-			}
-
-			@Override
-			public boolean onlyUnread() {
-				return false;
-			}
-
-			@Override
-			public boolean onlyDownloaded() {
-				return false;
-			}
-
-			@Override
-			public MessageType[] types() {
-				return null;
-			}
-
-			@Override
-			public int[] contentTypes() {
-				return null;
-			}
-		});
-
-		if (messageModels != null && !messageModels.isEmpty()) {
-			return messageId.toString().equals(messageModels.get(0).getApiMessageId());
-		}
-
-		return false;
-	}
-
 }

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

@@ -320,7 +320,7 @@ public class QuoteUtil {
 	 * @param messageModel
 	 * @return true if the message can be quoted, false otherwise
 	 */
-	public static boolean isQuoteable(AbstractMessageModel messageModel) {
+	public static boolean isQuoteable(@NonNull AbstractMessageModel messageModel) {
 		switch (messageModel.getType()) {
 			case IMAGE:
 			case FILE:

+ 0 - 51
app/src/main/java/ch/threema/app/utils/SMSUtil.java

@@ -1,51 +0,0 @@
-/*  _____ _
- * |_   _| |_  _ _ ___ ___ _ __  __ _
- *   | | | ' \| '_/ -_) -_) '  \/ _` |_
- *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
- *
- * Threema for Android
- * Copyright (c) 2013-2023 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.telephony.SmsMessage;
-
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public abstract class SMSUtil {
-
-	/**
-	 * Parse a cursor and create a threema sms if the sender is "threema" and the code in the body
-	 *
-	 * @param smsMessage
-	 * @return
-	 */
-	public static String getCodeFromMessage(SmsMessage smsMessage) {
-		String body = smsMessage.getDisplayMessageBody();
-
-		Pattern codePattern = Pattern.compile(".*https://myid.threema.ch/l/vm\\?code=(\\d{6})", Pattern.CASE_INSENSITIVE);
-		Matcher m = codePattern.matcher(body);
-		if (m.find()) {
-			String code = m.group(1);
-			if(code.length() > 0) {
-				return code;
-			}
-		}
-
-		return null;
-	}
-}

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

@@ -56,7 +56,7 @@ public class SoundUtil {
 				.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
 				.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
 				.build());
-			mediaPlayer.setVolume(0.3f, 0.3f);
+			mediaPlayer.setVolume(0.1f, 0.1f);
 			mediaPlayer.setStateListener(new MediaPlayerStateWrapper.StateListener() {
 				@Override
 				public void onCompletion(MediaPlayer mp) {
@@ -98,7 +98,7 @@ public class SoundUtil {
 	 */
 	public static AudioAttributes getAudioAttributesForCallNotification() {
 		return new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
-			.setUsage(AudioAttributes.USAGE_NOTIFICATION)
+			.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
 			.setFlags(FLAG_BYPASS_INTERRUPTION_POLICY)
 			.build();
 	}

+ 9 - 4
app/src/main/java/ch/threema/app/voicemessage/VoiceRecorderActivity.java

@@ -376,6 +376,7 @@ public class VoiceRecorderActivity extends AppCompatActivity implements DefaultL
 		if (mediaRecorder != null) {
 			mediaRecorder.reset();   // clear recorder configuration
 			mediaRecorder.release(); // release the recorder object
+			logger.info("MediaRecorder released {}", mediaRecorder);
 			mediaRecorder = null;
 		}
 	}
@@ -484,13 +485,13 @@ public class VoiceRecorderActivity extends AppCompatActivity implements DefaultL
 
 		audioRecorder = new AudioRecorder(this);
 		audioRecorder.setOnStopListener(this);
-		logger.info("new audioRecorder instance {}", audioRecorder);
 		try {
+			logger.info("Now recording to {}", uri);
 			mediaRecorder = audioRecorder.prepare(uri, MAX_VOICE_MESSAGE_LENGTH_MILLIS,
 				scoAudioState == AudioManager.SCO_AUDIO_STATE_CONNECTED ?
 				BLUETOOTH_SAMPLING_RATE_HZ :
 				getDefaultSamplingRate());
-			logger.info("Started recording with mediaRecorder instance {}", this.mediaRecorder);
+			logger.info("Started recording with {}", this.mediaRecorder);
 			if (mediaRecorder != null) {
 				startTimestamp = System.nanoTime();
 				mediaRecorder.start();
@@ -526,6 +527,7 @@ public class VoiceRecorderActivity extends AppCompatActivity implements DefaultL
 			// stop recording and release recorder
 			try {
 				if (mediaRecorder != null) {
+					logger.info("Stopped recording with {}", mediaRecorder);
 					mediaRecorder.stop();  // stop the recording
 				}
 				recordingDuration = getRecordingDuration() + 1;
@@ -557,7 +559,7 @@ public class VoiceRecorderActivity extends AppCompatActivity implements DefaultL
 					try {
 						mediaRecorder.pause();  // pause the recording
 					} catch (Exception e) {
-						logger.warn(
+						logger.error(
 							"Unexpected MediaRecorder Exception while pausing recording audio",
 							e
 						);
@@ -571,7 +573,7 @@ public class VoiceRecorderActivity extends AppCompatActivity implements DefaultL
 					try {
 						mediaPlayer.pause();  // pause the recording
 					} catch (Exception e) {
-						logger.warn(
+						logger.error(
 							"Unexpected MediaRecorder Exception while pausing playing audio",
 							e
 						);
@@ -610,6 +612,7 @@ public class VoiceRecorderActivity extends AppCompatActivity implements DefaultL
 	 * @return Duration in ms or 0 if the media player was unable to open this file
 	 */
 	private int getDurationFromFile() {
+		logger.info("Attempting to retrieve duration from file {}", uri);
 		MediaPlayer durationCheckMediaPlayer = MediaPlayer.create(this, uri);
 		if (durationCheckMediaPlayer != null) {
 			int duration = durationCheckMediaPlayer.getDuration();
@@ -617,6 +620,8 @@ public class VoiceRecorderActivity extends AppCompatActivity implements DefaultL
 				logger.info("Duration check returned 0");
 			}
 			durationCheckMediaPlayer.release();
+			logger.info("Duration in ms {}", duration);
+
 			return duration;
 		}
 		logger.info("Unable to create a media player for checking size. File already deleted by OS?");

+ 9 - 0
app/src/main/java/ch/threema/app/voip/services/VoipStateService.java

@@ -1566,6 +1566,7 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 		final Uri ringtoneUri = this.ringtoneService.getVoiceCallRingtone(messageReceiver.getUniqueIdString());
 
 		if (ringtoneUri != null) {
+			logger.info("Ringtone Uri = {}", ringtoneUri);
 			if (ringtonePlayer != null) {
 				stopRingtone();
 			}
@@ -1585,6 +1586,7 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 						if (ringtonePlayer != null) {
 							try {
 								ringtonePlayer.start();
+								logger.info("Ringtone player playing {}", ringtoneUri);
 							} catch (IllegalStateException e) {
 								logger.error("Unable to play ringtone", e);
 							}
@@ -1602,14 +1604,21 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 					ringtonePlayer.setDataSource(appContext, ringtoneUri);
 					ringtonePlayer.prepareAsync();
 				} catch (Exception e) {
+					logger.error("Exception preparing ringtone player", e);
 					stopRingtone();
 				}
+			} else {
+				logger.info("Not playing ringtone. isMuted = {}, isSystemMuted = {}", isMuted, isSystemMuted);
 			}
+		} else {
+			logger.info("No ringtone selected");
 		}
 	}
 
 	private synchronized void stopRingtone() {
 		if (ringtonePlayer != null) {
+			logger.info("Stopping ringtone player");
+
 			ringtonePlayer.stop();
 			ringtonePlayer.reset();
 			ringtonePlayer.release();

+ 0 - 11
app/src/main/res/drawable/bubble_compose_surface_variant.xml

@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="rectangle">
-    <corners android:radius="@dimen/edittext_bubble_border_radius" />
-    <padding
-        android:bottom="@dimen/chat_bubble_border_padding"
-        android:left="@dimen/chat_bubble_border_padding"
-        android:right="@dimen/chat_bubble_border_padding"
-        android:top="@dimen/chat_bubble_border_padding" />
-    <solid android:color="?attr/colorSurfaceVariant" />
-</shape>

+ 119 - 0
app/src/main/res/layout-land/activity_permission_request.xml

@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+	android:orientation="vertical"
+	android:gravity="center_horizontal"
+	android:animateLayoutChanges="true"
+	tools:context=".activities.PermissionRequestActivity">
+
+	<LinearLayout
+        android:id="@+id/permission_progress"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+		android:orientation="vertical"
+		android:gravity="center_horizontal"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintWidth_percent="0.2"/>
+
+	<TextView
+		android:id="@+id/permission_title"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_marginTop="16dp"
+		android:gravity="center_horizontal|bottom"
+		android:textAlignment="center"
+		style="@style/Threema.TextAppearance.Title"
+		app:layout_constraintTop_toTopOf="parent"
+		app:layout_constraintStart_toEndOf="@id/permission_progress"
+		app:layout_constraintEnd_toEndOf="parent"/>
+
+	<ScrollView
+		android:layout_width="0dp"
+		android:layout_height="0dp"
+		android:layout_marginTop="16dp"
+		android:padding="16dp"
+		app:layout_constraintTop_toBottomOf="@id/permission_title"
+		app:layout_constraintStart_toEndOf="@id/permission_progress"
+		app:layout_constraintBottom_toTopOf="@id/permission_continue"
+		app:layout_constraintEnd_toEndOf="parent">
+
+		<LinearLayout
+			android:id="@+id/bottom_linear_layout"
+			android:layout_width="match_parent"
+			android:layout_height="wrap_content"
+			android:orientation="vertical">
+
+			<TextView
+				android:id="@+id/permission_description"
+				android:layout_width="match_parent"
+				android:layout_height="wrap_content"
+				android:gravity="center_horizontal|bottom"
+				android:textAlignment="center"
+				style="@style/Threema.TextAppearance.BodyLarge" />
+
+			<TextView
+				android:id="@+id/permission_settings_explanation"
+				android:layout_width="match_parent"
+				android:layout_height="wrap_content"
+				android:layout_marginTop="16dp"
+				android:gravity="center_horizontal|bottom"
+				android:textAlignment="center"
+				style="@style/Threema.TextAppearance.BodyLarge" />
+		</LinearLayout>
+
+	</ScrollView>
+
+	<com.google.android.material.button.MaterialButton
+		android:id="@+id/permission_continue"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:text="@string/next"
+		style="@style/Threema.MaterialButton.Action"
+		app:layout_constraintStart_toEndOf="@id/permission_progress"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintBottom_toTopOf="@id/ignore_permission" />
+
+	<com.google.android.material.button.MaterialButton
+		android:id="@+id/grant_permission_settings"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:text="@string/grant_permission_settings"
+		style="@style/Threema.MaterialButton.Action"
+		app:layout_constraintStart_toEndOf="@id/permission_progress"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintBottom_toTopOf="@id/ignore_permission" />
+
+	<com.google.android.material.button.MaterialButton
+		android:id="@+id/grant_permission"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/grant_permission"
+        style="@style/Threema.MaterialButton.Action"
+		app:layout_constraintStart_toEndOf="@id/permission_progress"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/ignore_permission" />
+
+	<com.google.android.material.button.MaterialButton
+		android:id="@+id/ignore_permission"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:text="@string/ignore_permission"
+		style="@style/Threema.MaterialButton.Minimal"
+		app:layout_constraintBottom_toTopOf="@id/skip_permission"
+		app:layout_constraintStart_toEndOf="@id/permission_progress"
+		app:layout_constraintEnd_toEndOf="parent" />
+
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/skip_permission"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/use_threema_without_this_permission"
+        style="@style/Threema.MaterialButton.Minimal"
+        app:layout_constraintBottom_toBottomOf="parent"
+	    app:layout_constraintStart_toEndOf="@id/permission_progress"
+        app:layout_constraintEnd_toEndOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 34 - 29
app/src/main/res/layout/activity_permission_request.xml

@@ -6,15 +6,13 @@
     android:layout_height="match_parent"
 	android:orientation="vertical"
 	android:gravity="center_horizontal"
-	android:clipChildren="false"
-	android:clipToPadding="false"
 	android:animateLayoutChanges="true"
 	tools:context=".activities.PermissionRequestActivity">
 
 	<LinearLayout
         android:id="@+id/permission_progress"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
+        android:layout_height="0dp"
 		android:orientation="horizontal"
 		android:gravity="center_vertical"
 		app:layout_constraintTop_toTopOf="parent"
@@ -34,34 +32,41 @@
 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintVertical_bias="0.4" />
 
-	<LinearLayout
-        android:id="@+id/bottom_linear_layout"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="vertical"
-        android:layout_margin="16dp"
-        app:layout_constraintTop_toBottomOf="@id/permission_title"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toEndOf="parent">
+	<ScrollView
+		android:layout_width="0dp"
+		android:layout_height="0dp"
+		android:layout_marginTop="16dp"
+		android:padding="16dp"
+		app:layout_constraintTop_toBottomOf="@id/permission_title"
+		app:layout_constraintBottom_toTopOf="@id/permission_continue"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintEnd_toEndOf="parent">
+
+		<LinearLayout
+			android:id="@+id/bottom_linear_layout"
+			android:layout_width="match_parent"
+			android:layout_height="wrap_content"
+			android:orientation="vertical">
+
+			<TextView
+				android:id="@+id/permission_description"
+				android:layout_width="match_parent"
+				android:layout_height="wrap_content"
+				android:gravity="center_horizontal|bottom"
+				android:textAlignment="center"
+				style="@style/Threema.TextAppearance.BodyLarge" />
 
-		<TextView
-	        android:id="@+id/permission_description"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="16dp"
-            android:gravity="center_horizontal|bottom"
-            android:textAlignment="center"
-	        style="@style/Threema.TextAppearance.BodyLarge" />
+			<TextView
+				android:id="@+id/permission_settings_explanation"
+				android:layout_width="match_parent"
+				android:layout_height="wrap_content"
+				android:layout_marginTop="16dp"
+				android:gravity="center_horizontal|bottom"
+				android:textAlignment="center"
+				style="@style/Threema.TextAppearance.BodyLarge" />
+		</LinearLayout>
 
-        <TextView
-	        android:id="@+id/permission_settings_explanation"
-	        android:layout_width="match_parent"
-	        android:layout_height="wrap_content"
-	        android:layout_marginTop="16dp"
-	        android:gravity="center_horizontal|bottom"
-	        android:textAlignment="center"
-	        style="@style/Threema.TextAppearance.BodyLarge" />
-	</LinearLayout>
+	</ScrollView>
 
 	<com.google.android.material.button.MaterialButton
 		android:id="@+id/permission_continue"

+ 8 - 8
app/src/main/res/layout/activity_storagemanagement.xml

@@ -171,14 +171,14 @@
 						  android:layout_marginRight="6dp"
 						  android:background="@color/material_red"/>
 
-					<TextView android:layout_width="wrap_content"
-							  android:layout_height="wrap_content"
-							  android:layout_toRightOf="@id/legend_usage"
-							  android:layout_toLeftOf="@+id/usage_view"
-							  android:ellipsize="end"
-							  android:text="@string/storage_threema"
-						android:textAppearance="?android:textAppearanceMedium"
-						/>
+					<TextView
+						android:id="@+id/used_by_threema"
+						android:layout_width="wrap_content"
+						android:layout_height="wrap_content"
+						android:layout_toLeftOf="@+id/usage_view"
+						android:layout_toRightOf="@id/legend_usage"
+						android:ellipsize="end"
+						android:textAppearance="?android:textAppearanceMedium" />
 
 					<TextView android:id="@+id/usage_view"
 							  android:layout_width="wrap_content"

+ 19 - 11
app/src/main/res/layout/fragment_ballot_wizard1.xml

@@ -36,34 +36,42 @@
 		android:layout_width="match_parent"
 		android:layout_height="@dimen/ballotchoice_list_entry_height"
 		android:layout_marginBottom="@dimen/ballotchoice_bottom_margin"
-		android:paddingLeft="16dp"
-		android:paddingRight="16dp"
+		android:paddingLeft="8dp"
+		android:paddingRight="8dp"
 		android:layout_marginTop="2dp"
 		android:background="?android:attr/colorBackground"
 		android:minHeight="@dimen/ballotchoice_list_entry_height">
 
-		<ch.threema.app.ui.ComposeEditText
-			android:id="@+id/create_choice_name"
+		<com.google.android.material.textfield.TextInputLayout
+			style="@style/Threema.TextInputLayout.Compose"
+			android:id="@+id/textinputlayout_compose"
 			android:layout_width="match_parent"
 			android:layout_height="wrap_content"
 			android:layout_alignParentLeft="true"
 			android:layout_centerVertical="true"
-			android:layout_marginLeft="0dp"
-			android:layout_marginRight="4dp"
-			android:layout_toLeftOf="@+id/add_date"
+			android:layout_toLeftOf="@+id/create_choice"
+			android:layout_gravity="bottom"
+			android:outlineProvider="none">
+
+		<ch.threema.app.ui.ComposeEditText
+			style="@style/Threema.EditText.Compose"
+			android:id="@+id/create_choice_name"
+			android:layout_width="match_parent"
+			android:layout_height="wrap_content"
 			android:hint="@string/ballot_choice_add"
 			android:imeActionId="@integer/ime_wizard_add_choice"
 			android:imeActionLabel="+"
 			android:minHeight="@dimen/input_text_height"
-			android:paddingLeft="12dp"
+			android:paddingLeft="16dp"
 			android:paddingTop="3dp"
-			android:paddingRight="12dp"
+			android:paddingRight="80dp"
 			android:paddingBottom="4dp"
 			android:nextFocusDown="@id/create_choice_name"
 			android:singleLine="true"
 			android:textSize="16sp"
-			android:inputType="text"
-			android:background="@drawable/bubble_compose_surface_variant" />
+			android:inputType="text" />
+
+		</com.google.android.material.textfield.TextInputLayout>
 
 		<ImageButton
 			style="?android:attr/borderlessButtonStyle"

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

@@ -297,6 +297,7 @@
 
 				<com.google.android.material.textfield.TextInputLayout
 					style="@style/Threema.TextInputLayout.Compose"
+					android:id="@+id/textinputlayout_compose"
 					android:layout_width="match_parent"
 					android:layout_height="wrap_content"
 					android:layout_gravity="bottom"

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

@@ -38,6 +38,7 @@
 				android:visibility="gone"/>
 
 			<com.google.android.material.progressindicator.CircularProgressIndicator
+				style="@style/Widget.Material3.CircularProgressIndicator.Small"
 				android:id="@+id/session_loading"
 				android:indeterminate="true"
 				android:layout_width="@dimen/avatar_size_small"

+ 1 - 2
app/src/main/res/layout/popup_mention_selector.xml

@@ -2,12 +2,11 @@
 <com.google.android.material.card.MaterialCardView
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    style="@style/Threema.CardView.TopRounded"
+    style="@style/Threema.CardView.MentionSelector"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:layout_gravity="bottom"
     android:gravity="bottom"
-    app:cardElevation="2dp"
     app:contentPadding="0dp"
     app:strokeWidth="0dp"
     app:cardPreventCornerOverlap="false"

+ 10 - 0
app/src/main/res/layout/view_empty.xml

@@ -2,6 +2,16 @@
 
 <merge xmlns:android="http://schemas.android.com/apk/res/android">
 
+	<com.google.android.material.progressindicator.CircularProgressIndicator
+			style="@style/Widget.Material3.CircularProgressIndicator.Medium"
+			android:indeterminate="true"
+			android:id="@+id/loading_view"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_gravity="center"
+			android:gravity="center"
+			android:visibility="gone"/>
+
 	<TextView
 			  android:id="@+id/empty_text"
 			  android:layout_width="wrap_content"

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

@@ -3,6 +3,7 @@
 	android:layout_width="60dp"
 	android:layout_height="60dp"
 	android:padding="5dp"
+	android:layout_gravity="center_horizontal"
 	xmlns:app="http://schemas.android.com/apk/res-auto">
 
 	<!-- This frame layout is needed for backwards compatibility before api 23 -->

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

@@ -539,7 +539,7 @@
     <string name="prefs_sum_passphrase">Патрабаваць кодавую фразу для разблакавання лакальнага шыфравання</string>
     <string name="prefs_title_masterkey_change_passphrase">Змяніць кодавую фразу</string>
     <string name="storage_total">Месца на ўнутраным сховішчы</string>
-    <string name="storage_threema">Ужываецца Threema</string>
+    <string name="storage_threema">Ужываецца %s</string>
     <string name="storage_total_free">Усяго вольнага месца</string>
     <string name="storage_total_in_use">Ужываецца</string>
     <string name="one_year">1 года</string>
@@ -1366,10 +1366,11 @@
     <string name="forward_security_mode">Perfect Forward Secrecy</string>
     <string name="forward_security_mode_none">не</string>
     <string name="clear_forward_security">Скінуць Perfect Forward Secrecy для гэтага кантакту</string>
+    <string name="clear_forward_security_warning">Выконвайце гэтае дзеянне толькі па інструкцыі ад службы падтрымкі Threema.</string>
     <string name="forward_security_cleared">Сеансы Perfect Forward Secrecy з гэтым кантактам ачышчаны</string>
     <string name="message_without_forward_security">Атрымана паведамленне без Perfect Forward Secrecy.  Пераканайцеся, што адпраўнік наўмысна адключыў Perfect Forward Secrecy.</string>
     <string name="forward_security_established">Паведамленні ў гэтым чаце цяпер абаронены Perfect Forward Secrecy.</string>
-    <string name="forward_security_established_rx">Паведамленні, атрыманыя ў гэтым чаце, цяпер абаронены Perfect Forward Secrecy.  Уключыце Perfect Forward Secrecy у кантактных дадзеных, каб таксама абараніць адпраўленыя паведамленні.</string>
+    <string name="forward_security_established_rx">Паведамленні, атрыманыя ў гэтым чаце, цяпер абаронены Perfect Forward Secrecy.</string>
     <string name="forward_security_reset">Сеанс Perfect Forward Secrecy быў скінуты.  Гэта можа адбыцца, калі партнёр па чаце мяняе прылады або пераўсталёўвае праграму.</string>
     <string name="forward_security_reset_simple">Сеанс Perfect Forward Secrecy з гэтым кантактам быў скінуты.</string>
     <string name="forward_security_message_out_of_order">Паведамленне было атрымана не ўшкаджонае і не можа быць расшыфравана.</string>
@@ -1416,7 +1417,13 @@
     <string name="group_call">Супольны званок</string>
     <string name="checking_compatibility">Праверка сумяшчальнасці…</string>
     <string name="group_call_mic_permission_rationale">Для выканання супольных выклікаў вы павінны дазволіць %s доступ да мікрафона ў «наладах».</string>
+    <string name="group_call_mic_permission_description">Каб выконваць супольныя выклікі, вы павінны дазволіць доступ да мікрафона, каб іншыя маглі вас пачуць.</string>
+    <string name="call_mic_permission_description">Каб рабіць выклікі, вы павінны дазволіць доступ да мікрафона, каб іншыя маглі вас пачуць.</string>
     <string name="group_call_camera_permission_rationale">Каб іншыя ўдзельнікі маглі бачыць ваш відарыс, вы павінны дазволіць %s доступ да камеры ў «наладах».</string>
+    <string name="group_call_phone_permission_description">Для выканання супольных выклікаў неабходны дазвол да тэлефона, каб вызначыць, калі іншы выклік перапыняе супольны выклік.</string>
+    <string name="call_phone_permission_description">Для выканання выклікаў Threema неабходны дазвол да тэлефона, каб вызначыць, калі іншы выклік перапыняе выклік Threema.</string>
+    <string name="group_call_nearby_devices_permission_description">Каб выконваць супольныя выклікі з дапамогай гарнітуры Bluetooth, вы павінны дазволіць «прыладам паблізу» выяўляць гарнітуры Bluetooth.</string>
+    <string name="call_nearby_devices_permission_description">Для выканання выклікаў Threema з гарнітурай bluetooth вы павінны дазволіць «прыладам паблізу» выяўляць гарнітуры bluetooth.</string>
     <string name="settings">Налады</string>
     <string name="leave">Пакінуць</string>
     <string name="fs_key_mismatch">несупадзенне ключоў</string>
@@ -1445,6 +1452,20 @@
     <string name="voice_message_from">Галасавое паведамленне ад %s</string>
     <string name="vm_fg_service_not_allowed">Памылка прайграв. галасавога паведамлення</string>
     <string name="vm_fg_service_not_allowed_explain">Націсніце кнопку прайгравання ў медыяапавяшчэнні, калі яно ўсё яшчэ прысутнічае, у адваротным выпадку адкрыйце праграму, каб пачаць прайграванне з чата.</string>
+    <string name="use_threema_without_this_permission">Выкарыстоўвайце %s без гэтага дазволу</string>
+    <string name="grant_permission">Даць дазвол</string>
+    <string name="grant_permission_settings">Дайце дазвол у наладах</string>
+    <string name="ignore_permission">Больш не пытайцеся</string>
+    <string name="permission_nearby_devices">Прылады паблізу</string>
+    <string name="permission_bluetooth">Bluetooth</string>
+    <string name="permission_camera">Камера</string>
+    <string name="permission_microphone">Мікрафон</string>
+    <string name="permission_read_phone_state">Тэлефон</string>
+    <string name="permission_enable_in_settings_rationale">Уключыце дазвол у наладах.  Выберыце «Дазволы», а потым дазвольце «%s».</string>
+    <string name="group_name">Назва суполкі</string>
+    <string name="samsung_permission_problem_explain">%1$s не можа атрымаць доступ да медыяфайлаў з-за адсутнасці дазволу ў сістэмнай праграме.  Каб выправіць гэта, упэўніцеся, што пераключальнік побач з \"Знешняе назапашвальнік\" уключаны на наступным экране, і перазапусціце %1$s.</string>
+    <string name="min_n_chars">мін.  %d сімвалаў</string>
+    <string name="prefs_title_grant_bluetooth_permission">Дайце дазвол Bluetooth</string>
     <plurals name="contacts_counter_label">
         <item quantity="few">%d кантактаў</item>
         <item quantity="many">%d кантакты</item>

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

@@ -534,7 +534,7 @@ de contacte</string>
     <string name="prefs_sum_passphrase">Requerir una contrasenya per desbloquejar el xifrat local</string>
     <string name="prefs_title_masterkey_change_passphrase">Canviar la contrasenya</string>
     <string name="storage_total">Espai a l\'emmagatzematge intern</string>
-    <string name="storage_threema">Usat per Threema</string>
+    <string name="storage_threema">Usat per %s</string>
     <string name="storage_total_free">Espai lliure total</string>
     <string name="storage_total_in_use">Usat</string>
     <string name="one_year">1 any</string>

+ 75 - 69
app/src/main/res/values-cs/strings.xml

@@ -370,11 +370,11 @@
     <string name="add_acount_from_within_threema">Spravujte své Threema ID přímo v aplikaci Threema.</string>
     <string name="save_image">Uložit obrázek</string>
     <string name="share_image">Sdílet obrázek</string>
-    <string name="view_in_gallery">Zobrazit v Galerii</string>
+    <string name="view_in_gallery">Zobrazit v galerii</string>
     <string name="token_register_failed">Aktualizace push tokenu se nezdařila. Zkuste to prosím později.</string>
     <string name="internet_connection_required">Žádné připojení k Internetu.</string>
-    <string name="prefs_sum_save_media">Automaticky stahovat, dešifrovat a ukládat příchozí obrázky a videa, včetně snímků pořízených fotoaparátem, do Galerie v nezašifrované podobě</string>
-    <string name="prefs_save_media">Automaticky ukládat do Galerie</string>
+    <string name="prefs_sum_save_media">Automaticky stahovat, dešifrovat a ukládat příchozí obrázky a videa, včetně snímků pořízených fotoaparátem, do galerie v nezašifrované podobě</string>
+    <string name="prefs_save_media">Automaticky ukládat do galerie</string>
     <string name="title_add_distribution_list">Nový distribuční seznam</string>
     <string name="title_edit_distribution_list">Upravit distribuční seznam</string>
     <string name="really_delete_distribution_list">Odstranit distribuční seznam</string>
@@ -399,8 +399,8 @@
     <string name="group_was_synchronized">Skupina synchronizována.</string>
     <string name="verification_level2_work_explain">"Interní kontakt, předem obsazený vaší organizací.
 "</string>
-    <string name="verification_level3_work_explain">Interní kontakt, jehož identitu a veřejný klíč jste osobně ověřili naskenováním jejich QR kódu.</string>
-    <string name="verification_level3_explain">Kontakt, jehož identitu a veřejný klíč jste osobně ověřili naskenováním QR kódu.</string>
+    <string name="verification_level3_work_explain">Interní kontakt, jehož identitu a veřejný klíč jste osobně ověřili naskenováním jeho QR kódu.</string>
+    <string name="verification_level3_explain">Kontakt, jehož identitu a veřejný klíč jste osobně ověřili naskenováním jeho QR kódu.</string>
     <string name="verification_level2_explain">Kontakt, jehož telefonní číslo a/nebo e‑mailová adresa je ve vašem adresáři.</string>
     <string name="verification_level1_explain">Neznámý kontakt; buď tento kontakt nepropojil telefonní číslo ani e‑mailovou adresu ke svému ID, nebo váš adresář údaje o tomto kontaktu neobsahuje.</string>
     <string name="state_dialog_received">Přijato</string>
@@ -412,7 +412,7 @@
     <string name="media_gallery_all">Vše</string>
     <string name="media_gallery_pictures">Obrázky</string>
     <string name="media_gallery_videos">Video soubory</string>
-    <string name="group_membership_title">Je členem uvedených skupin</string>
+    <string name="group_membership_title">Je členem těchto skupin</string>
     <string name="group_description">Popis skupiny</string>
     <string name="change_group_description">Změnit popis skupiny</string>
     <string name="add_group_description">Přidat popis skupiny</string>
@@ -465,7 +465,7 @@ http://www.7-zip.org nebo https://itunes.apple.com/us/app/the-unarchiver/id42542
     <string name="really_block_contact">Budoucí zprávy od tohoto kontaktu budou zahazovány. Přejete si přesto pokračovat?</string>
     <string name="ballot_result_final">Výsledek ankety</string>
     <string name="invalid_cannot_send">Nemůžete odesílat zprávy neplatnému kontaktu</string>
-    <string name="ballot_answer_count_error">Zadejte prosím do ankety alespoň dvě odpovědi.</string>
+    <string name="ballot_answer_count_error">Zadejte prosím do ankety alespoň dvě možnosti.</string>
     <string name="ballot_one_contact_not_supported">Upozornění: %1$s se nebude moci zúčastnit vaší ankety.</string>
     <string name="ballot_x_contact_not_supported">Upozornění: Následující počet kontaktů: %1$d se nebude moci zúčastnit vaší ankety.</string>
     <string name="contact_state_inactive">Neaktivní</string>
@@ -476,7 +476,7 @@ http://www.7-zip.org nebo https://itunes.apple.com/us/app/the-unarchiver/id42542
     <string name="message_acknowledged">Souhlas odeslán</string>
     <string name="push_disable_text">Jestliže budete pokračovat, bude použita služba „Threema Push“ namísto systémové služby push. Toto pomáhá redukovat vaši digitální stopu, ale vyžaduje to také, aby vaše zařízení bylo nakonfigurováno takovým způsobem, aby aplikace mohla stále běžet na pozadí. V případě potřeby kontaktujte podporu výrobce vašeho zařízení, aby vám pomohla s konfigurací, jestliže odpovídající možnosti nejsou k dispozici v nastavení zařízení.</string>
     <string name="ballot_intermediate_results_show">Zobrazovat průběžné výsledky</string>
-    <string name="converting_video">Zpracování videa</string>
+    <string name="converting_video">Zpracování videa</string>
     <string name="video_size_small">Velká (náhledová kvalita)</string>
     <string name="video_size_large">Mírná (kvalitnější obraz)</string>
     <string name="video_size_original">Žádná (datově náročná)</string>
@@ -491,7 +491,7 @@ http://www.7-zip.org nebo https://itunes.apple.com/us/app/the-unarchiver/id42542
     <string name="qr_code">QR kód</string>
     <string name="really_leave_id_export">Pokud jste tak ještě neučinili, uložte textový záložní řetězec vašeho ID nebo jemu odpovídající QR kód na bezpečné místo, nebo jej vytiskněte. Threema ID je vaše identita, která nemůže být bez zálohy nijak obnovena.</string>
     <string name="revocation_key_title">Zrušení/odvolání ID</string>
-    <string name="revocation_key_not_set">Není nastaveno žádné heslo pro odvolání ID</string>
+    <string name="revocation_key_not_set">Není nastaveno žádné heslo pro zrušení/odvolání ID</string>
     <string name="revocation_key_set_at">Heslo nastaveno %1$s</string>
     <string name="prefs_sum_remove_wallpapers">Odstraní všechny individuální tapety</string>
     <string name="prefs_title_remove_wallpapers">Odstranit všechny tapety</string>
@@ -541,7 +541,7 @@ https://myid.threema.ch/revoke</string>
     <string name="prefs_sum_passphrase">Vyžadovat heslo k odemknutí lokálních zašifrovaných dat</string>
     <string name="prefs_title_masterkey_change_passphrase">Změna hesla</string>
     <string name="storage_total">Prostor na vnitřním úložišti</string>
-    <string name="storage_threema">Využito aplikací Threema</string>
+    <string name="storage_threema">Využito aplikací %s</string>
     <string name="storage_total_free">Celkové volné místo</string>
     <string name="storage_total_in_use">Využito</string>
     <string name="one_year">1 rok</string>
@@ -578,7 +578,7 @@ možné je obnovit.</string>
     <string name="privacy_policy">Zásady ochrany osobních údajů</string>
     <string name="terms_of_service">Podmínky služby</string>
     <string name="eula">Licenční smlouva s koncovým uživatelem</string>
-    <string name="save_group_changes">Přejete si uložit provedené změny ve skupině?</string>
+    <string name="save_group_changes">Přejete si uložit změny provedené ve skupině?</string>
     <string name="prefs_title_fontsize">Velikost textu</string>
     <string name="fontsize_normal">Základní</string>
     <string name="fontsize_large">Velký</string>
@@ -614,10 +614,10 @@ možné je obnovit.</string>
     <string name="new_wizard_choose_nickname">Zadejte svoji přezdívku</string>
     <string name="new_wizard_nickname_explain">Vaši přátelé v oznámeních uvidí vaši přezdívku</string>
     <string name="new_wizard_hint_enter_nickname">Zadejte přezdívku</string>
-    <string name="new_wizard_help_your_friends_find_you">Pomozte svým přátelům, aby vás mohli vyhledat!</string>
+    <string name="new_wizard_help_your_friends_find_you">Pomozte svým přátelům, aby vás mohli najít!</string>
     <string name="new_wizard_link_mobile">Propojte své mobilní číslo a/nebo e‑mail se svým Threema ID.</string>
     <string name="new_wizard_link_mobile_only">Propojte sv\u00e9 mobiln\u00ed \u010d\u00edslo se sv\u00fdm Threema\u00a0ID.</string>
-    <string name="new_wizard_hint_mobile_number">Číslo mobilního telefonu (nepovinné)</string>
+    <string name="new_wizard_hint_mobile_number">Číslo mobilního telefonu (volitelné)</string>
     <string name="new_wizard_hint_email">E‑mail (volitelné)</string>
     <string name="new_wizard_find_friends">Najděte vaše přátele na Threemě</string>
     <string name="new_wizard_sync_contacts_explain">Můžete zapnout synchronizaci, abyste viděli, kteří přátelé používají aplikaci Threema.</string>
@@ -634,21 +634,21 @@ možné je obnovit.</string>
     <string name="new_wizard_info_id">Vytvořili jste dvojici šifrovacích klíčů. Veřejný klíč byl bezpečně přenesen na naše servery. Soukromý klíč nikdy neopustí vaše zařízení. Tím je zajištěno, že nikdo nepovolaný se nebude moci dostat k vašim zprávám.</string>
     <string name="new_wizard_info_sync_contacts">Pokud tuto možnost povolíte, odešlou se jednosměrně šifrované (hashované) e‑mailové adresy a telefonní čísla z vašich kontaktů na náš server. Zde se porovnají s kontakty ostatních uživatelů. V případě shody se u vás kontakt zobrazí v adresáři.
 Na našich serverech neukládáme žádná data z vašeho adresáře.</string>
-    <string name="new_wizard_info_sync_contacts_dialog">Synchronizace kontaktů vám může pomoci najít vaše přátele automaticky. Pokud budete souhlasit, telefonní čísla a e‑mailové adresy z vašeho telefonního seznamu budou před odesláním na naše servery zašifrovány, aby mohly být vyhledány odpovídající kontakty. Žádná data nebudou uložena nebo sdílena.\n\nPřejete si povolit synchronizaci kontaktů?</string>
-    <string name="new_wizard_info_link">Poskytnete‑li svoje telefonní číslo a e‑mailovou adresu, Threema může vašim kontaktům zajistit automatické přidání vašeho ID do jejich adresářů. Data budou uložena jednosměrně šifrovaná (hashovaná) na našem serveru. Tento krok můžete přeskočit, pokud chcete používat aplikaci Threema zcela anonymně. Toto nastavení lze později změnit.</string>
+    <string name="new_wizard_info_sync_contacts_dialog">Synchronizace kontaktů vám může pomoci najít vaše přátele automaticky. Pokud budete souhlasit, telefonní čísla a e‑mailové adresy z vašeho telefonního seznamu budou před odesláním na naše servery zašifrovány, aby mohly být vyhledány shodující se kontakty. Žádná data nebudou uložena nebo sdílena.\n\nPřejete si povolit synchronizaci kontaktů?</string>
+    <string name="new_wizard_info_link">Poskytnete‑li svoje telefonní číslo a e‑mailovou adresu, Threema může pomoci vašim přátelům najít vás automaticky, pokud vás mají uložené v telefonním adresáři kontaktů. Data budou uložena jednosměrně šifrovaná (hashovaná) na našem serveru. Tento krok můžete přeskočit, pokud chcete používat aplikaci Threema zcela anonymně. Toto nastavení lze později změnit.</string>
     <string name="new_wizard_info_link_phone_only">Poskytnete‑li aplikaci Threema své telefonní číslo, umožníte tak svým
-přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého telefonu. Číslo bude na našem serveru uložené v jednosměrně šifrované podobě (hashované). Jestliže chcete používat aplikaci Threema zcela anonymně, můžete tento krok přeskočit.</string>
+přátelům vás automaticky najít, pokud vás mají v adresáři svého telefonu. Číslo bude na našem serveru uložené v jednosměrně šifrované (hashované) podobě. Jestliže chcete používat aplikaci Threema zcela anonymně, můžete tento krok přeskočit.</string>
     <string name="new_wizard_info_nickname">Přezdívka se na některých zařízeních používá pro push oznámení nebo jako další možnost vaší identifikace uživatelům, kteří vás dosud nemají ve svém adresáři. Doporučujeme zadat pouze vaše křestní jméno nebo pseudonym. Pokud nenastavíte žádnou přezdívku, použijeme ve výchozím nastavení vaše Threema ID. Toto nastavení lze později změnit.</string>
     <string name="not_linked">nepropojené</string>
     <string name="linked">propojené</string>
     <string name="pending_sms_verification_notice">Vaše číslo mobilního telefonu zatím nebylo ověřeno.</string>
     <string name="no_sms_received">Nepřišla vám SMS?</string>
-    <string name="really_cancel_verify">Skutečně si přejete přerušit proces ověření mobilního čísla?</string>
+    <string name="really_cancel_verify">Skutečně si přejete přerušit proces ověření čísla mobilního telefonu?</string>
     <string name="verification_of">Ověření %s</string>
     <string name="status_ballot_voting_changed">V anketě „%1$s“ přibyl další hlas</string>
     <string name="status_ballot_user_first_vote">Člen „%1$s“ hlasoval pro „%2$s“</string>
     <string name="status_ballot_user_modified_vote">Člen „%1$s“ změnil hlas pro „%2$s“</string>
-    <string name="status_ballot_all_votes">Hlasy pro anketu „%1$s“ jsou kompletní</string>
+    <string name="status_ballot_all_votes">Hlasy v anketě „%1$s“ jsou kompletní</string>
     <string name="restore">Obnovit</string>
     <string name="new_wizard_anonymous_confirm">Nezadali jste ani číslo mobilního telefonu, ani e‑mailovou adresu, které mohou být propojené s vaším Threema ID. Nebudete se proto zobrazovat v seznamu kontaktů vašich přátel. Skutečně si přejete používat aplikaci Threema anonymně?</string>
     <string name="new_wizard_anonymous_confirm_phone_only">Nezadali jste číslo mobilního telefonu, které může být propojeno s vaším Threema ID. Nebudete se proto zobrazovat v seznamu kontaktů vašich přátel. Skutečně si přejete používat aplikaci Threema anonymně?</string>
@@ -668,7 +668,7 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
     <string name="select_all">Vybrat vše</string>
     <string name="deleting_messages">Odstraňování zpráv</string>
     <string name="media_gallery_files">Soubory</string>
-    <string name="prefs_gif_autoplay">Autom. přehrávat animované GIFy</string>
+    <string name="prefs_gif_autoplay">Automaticky přehrávat animované GIFy</string>
     <string name="media_gallery_audio">Hlasové zprávy</string>
     <string name="action_clone_group">Kopírovat skupinu</string>
     <string name="clone_group_message">Tímto vytvoříte kopii této skupiny, ve které budete správcem. Přejete si pokračovat?</string>
@@ -728,10 +728,10 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
     <string name="palette">Paleta</string>
     <string name="stickers">Samolepky</string>
     <string name="text">Text</string>
-    <string name="undo">Vrátit</string>
+    <string name="undo">Vrátit zpět</string>
     <string name="android_backup_date">Poslední záloha</string>
     <string name="check_now">Zkontrolovat</string>
-    <string name="discard">Vyřadit</string>
+    <string name="discard">Zahodit</string>
     <string name="android_backup_restart_threema">Vydržte. Aplikace bude restartována během několika sekund.</string>
     <string name="battery_optimizations_title">Vypnutí optimalizace baterie</string>
     <string name="battery_optimizations_explain">Optimalizace baterie zabraňuje funkci %1$s pracovat správně ve chvíli, kdy vaše zařízení přejde do režimu spánku. Vypněte prosím optimalizaci baterie pro aplikaci %2$s.</string>
@@ -739,7 +739,7 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
     <string name="battery_optimizations_disable_guide_ctd">Vyhledejte v seznamu aplikaci %s a vypněte pro ni optimalizaci baterie</string>
     <string name="battery_optimizations_disable_confirm">Skutečně si přejete ponechat aplikaci %1$s optimalizaci baterie zapnutou? %2$s nebude pracovat správně.</string>
     <string name="enter_text_hint">Zadejte text</string>
-    <string name="backup_explain_text">Pokud mobil vyměníte nebo ho ztratíte, nikdo již nedokáže obnovit vaše Threema ID ani konverzace bez provedené zálohy. Pravidelně proto pomocí odpovídajících možností zálohy vytvářejte a ukládejte je na bezpečné místo.</string>
+    <string name="backup_explain_text">Pokud zařízení vyměníte nebo ho ztratíte, nikdo již nedokáže obnovit vaše Threema ID ani konverzace bez provedené zálohy. Pravidelně proto pomocí odpovídajících možností zálohy vytvářejte a ukládejte je na bezpečné místo.</string>
     <string name="data_backup_explain">Zálohovaná data obsahují: \n\n&#9679; Vaše ID a šifrovací klíče \n&#9679; Kontakty a jejich úrovně ověření \n&#9679; Členství ve skupinách \n&#9679; Konverzace \n&#9679; Obrázky a jiné soubory (volitelné)\n\nData budou uložena do šifrovaného souboru ZIP. Po úspěšném vytvoření zálohy doporučujeme tento soubor překopírovat do jiného zařízení.</string>
     <string name="draw">Kreslit</string>
     <string name="edit">Upravit</string>
@@ -803,7 +803,7 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
     <string name="shortcut_choice_title">Vytvořit zástupce pro…</string>
     <string name="prefs_title_device_info">Informace o zařízení</string>
     <string name="notifications_disabled_title">Oznámení vypnuta</string>
-    <string name="notifications_disabled_text">Oznámení z aplikace Threema jsou zakázána v nastavení systému. Nebudete upozorňováni na nové zprávy.</string>
+    <string name="notifications_disabled_text">Oznámení z aplikace Threema jsou zakázána v nastavení systému. Nebudete upozorňován(a) na nové zprávy.</string>
     <string name="notifications_disabled_settings">Upravit nastavení systému</string>
     <string name="error_attaching_files">Nezdařilo se přidat přílohy.</string>
     <string name="prefs_fix_powermanager_problems">Zakázat úsporný režim</string>
@@ -846,11 +846,11 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
     <string name="switched_on">Zap.</string>
     <string name="title_tab_work_users">Uživatelé Threema Work</string>
     <string name="no_matching_work_contacts">Nebyly nalezeny žádné správcem ověřené Threema Work kontakty</string>
-    <string name="all">Vše</string>
+    <string name="all">Všem</string>
     <string name="webclient_session_stop_all">Zavřít vše</string>
     <string name="passphrase_service_name">Správa hesel</string>
     <string name="passphrase_service_description">Zobrazit oznámení, pokud je heslo odemknuto</string>
-    <string name="webclient_service_description">Zobrazit oznámení, pokud je desktopová/webová relace aktivní</string>
+    <string name="webclient_service_description">Zobrazit oznámení, pokud je aktivní desktopová/webová relace</string>
     <string name="prefs_title_accept_privacy_policy">Přijmout Zásady ochrany osobních údajů</string>
     <string name="privacy_policy_explain">%1$s dbá na vaše soukromí přísněji než kterýkoliv jiný komunikátor. Zjistěte více v našem ustanovení %2$s.</string>
     <string name="privacy_policy_check_confirm">Před použitím aplikace %s prosím odsouhlaste Zásady ochrany osobních údajů.\n\n(Vyžadováno směrnicí EU 2016/679.)</string>
@@ -989,7 +989,7 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
     <string name="my_id">Moje ID</string>
     <string name="profile_picture_and_nickname">Profilový obrázek a přezdívka</string>
     <string name="lp_select_this_place">Vybrat toto místo</string>
-    <string name="lp_or_select_nearby">Nebo si zvolte nějaké blízké místo</string>
+    <string name="lp_or_select_nearby">Nebo si vyberte nějaké místo poblíž</string>
     <string name="lp_use_this_location">Odeslat tuto polohu?</string>
     <string name="lp_search_place">Zadejte město nebo adresu</string>
     <string name="lp_no_nearby_places_found">Žádná blízká místa nebyla nalezena</string>
@@ -1012,7 +1012,7 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
     <string name="open_in_maps_app">Otevřít v aplikaci Mapy</string>
     <string name="delete">Odstranit</string>
     <string name="continue_recording">Pokračovat v nahrávání</string>
-    <string name="whatsnew_title">Vítejte v %1$s 5.1</string>
+    <string name="whatsnew_title">Vítejte v aplikaci %1$s 5.1</string>
     <string name="whatsnew_headline"><![CDATA[
 		<p>Tato aktualizace se zaměřuje na zlepšení zabezpečení, vyšší použitelnost a modernizaci aplikace.</p>
 		<p>Na základě četných žádostí byla aplikace kompletně přepracována, aby plně podporovala paradigma „Material You“ představené se systémem Android 12. Díky volitelnému nastavení „Dynamické barvy“ se aplikace může přizpůsobit vašemu personalizovanému barevnému schématu.</p>
@@ -1028,7 +1028,7 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
     <string name="pinning_not_trusted">Selhání při komunikaci s certifikátem. Zkontrolujte, zda je v úložišti vašeho zařízení (credentials storage) nainstalován a aktivován certifikát „Entrust Root Certification Authority - G2“.</string>
     <string name="pinning_failed">Selhání při komunikaci s certifikátem. Může probíhat útok Man‑in‑the‑middle. Pokud máte nainstalovaný blokátor reklam, filtr obsahu nebo aplikaci s firewallem, například „AdGuard“, zakažte ji prosím pro aplikaci Threema.</string>
     <string name="open_myid_popup">Otevře vyskakovací okno s podrobnostmi</string>
-    <string name="logo">Logo / Skok na začátek</string>
+    <string name="logo">Logo / Posunout na začátek</string>
     <string name="quote_subj_end">Ukončit citaci</string>
     <string name="quote_subj">Citace</string>
     <string name="duration">Doba trvání</string>
@@ -1042,14 +1042,14 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
     <string name="lp_search_place_min_chars">Chcete‑li vyhledat místo, zadejte alespoň tři znaky.</string>
     <string name="lp_search_place_no_matches">Nebyla nalezena žádná odpovídající místa. Upravte prosím váš dotaz.</string>
     <string name="wallpaper_default">Výchozí tapeta</string>
-    <string name="wallpaper_gallery">Vybrat z Galerie</string>
+    <string name="wallpaper_gallery">Vybrat z galerie</string>
     <string name="wallpaper_none">Prázdné pozadí</string>
     <string name="wallpaper_threema">Tapeta %s</string>
     <string name="message_id">ID zprávy</string>
     <string name="mime_type">Typ MIME</string>
     <string name="password_does_not_comply">Heslo není v souladu se zásadami, které stanovil správce.</string>
     <string name="audio_mute_due_to_focus_loss">Zvuk byl dočasně ztlumen kvůli ztrátě výhradní kontroly</string>
-    <string name="restore_data_backup_explain">Chcete‑li obnovit zálohovaná data, nejprve na obrazovce „Můj profil“ odstraňte své Threema ID.\n\nAž se aplikace znovu spustí, zvolte možnosti „Obnovit ze zálohy“, „Další možnosti obnovení“, „Záloha dat“ a následně vyberte soubor se zálohou dat, kterou chcete obnovit.</string>
+    <string name="restore_data_backup_explain">Chcete‑li obnovit zálohovaná data, nejdříve na obrazovce „Můj profil“ odstraňte své Threema ID.\n\nAž se aplikace znovu spustí, zvolte možnosti „Obnovit ze zálohy“, „Další možnosti obnovení“, „Záloha dat“ a následně vyberte soubor se zálohou dat, kterou chcete obnovit.</string>
     <string name="audio_focus_loss_complete">Hovor byl odpojen kvůli kompletní ztrátě výhradní kontroly.</string>
     <string name="tap_for_picture_hold_for_video">Klepnutím vyfotíte, podržením zaznamenáte video</string>
     <string name="sending_media">Odesílání médií</string>
@@ -1065,7 +1065,7 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
     <string name="prefs_work_time_start_sum">Čas zahájení</string>
     <string name="prefs_work_time_end">Konec pracovní doby</string>
     <string name="prefs_work_time_end_sum">Čas ukončení</string>
-    <string name="prefs_working_days_enable_title">Volba po pracovní době</string>
+    <string name="prefs_working_days_enable_title">Zásady po pracovní době</string>
     <string name="prefs_working_days_enable_sum">Zakázat oznámení a odmítat hovory mimo pracovní dobu</string>
     <string name="work_life_dnd_active">Pravidla po pracovní době aktivní</string>
     <string name="pencil">Tužka</string>
@@ -1153,7 +1153,7 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
     <!-- Abbreviation for "30 days", shown at the bottom of the contact list -->
     <string name="thirty_days_abbrev">30 d.</string>
     <string name="show_in_chat">Zobrazit v konverzaci</string>
-    <string name="group_create_no_members">Opravdu chcete vytvořit prázdnou skupinu?</string>
+    <string name="group_create_no_members">Opravdu si přejete vytvořit prázdnou skupinu?</string>
     <string name="notes">Poznámky</string>
     <string name="blur_faces">Rozmazat tváře</string>
     <string name="brush">Štětec</string>
@@ -1162,7 +1162,7 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
     <string name="smiley">Smajlík</string>
     <string name="blur">Rozmazat</string>
     <string name="face_blur_tooltip_title">Rozpoznávání tváří</string>
-    <string name="face_blur_tooltip_text">Nechte na obrázcích najít tváře a nechte je rozmazat nebo zakrýt smajlíkem.</string>
+    <string name="face_blur_tooltip_text">Nechte na obrázcích najít tváře a rozmazat je nebo zakrýt smajlíkem.</string>
     <string name="listened_to">vyslechnuta</string>
     <string name="transcoder_unsupported_audio_format">Zvuk nelze překódovat kvůli chybějící podpoře tohoto formátu v systému.</string>
     <string name="transcoder_unknown_audio_error">Během překódování zvuku došlo k neznámé chybě.</string>
@@ -1181,7 +1181,7 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
     <string name="group_join_request">Požadavek o připojení do skupiny</string>
     <string name="group_join_request_for">Požadavek o připojení do skupiny %s</string>
     <string name="group_link_default_name">Nepojmenovaný odkaz</string>
-    <string name="group_link_share">Sdílet odkaz do skupiny</string>
+    <string name="group_link_share">Sdílet odkaz na skupinu</string>
     <string name="group_request_incoming_dialog_title">Požadavek o připojení do skupiny od %s</string>
     <string name="accept">Přijmout</string>
     <string name="reject">Zamítnout</string>
@@ -1208,7 +1208,7 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
     <string name="new_group_link_success">Byl přidán nový odkaz</string>
     <string name="link_administration_off_explain">Tento odkaz již byl nakonfigurován a sdílen dříve. Změna této vlastnosti již není možná. Vygenerujte nový odkaz s takovými vlastnostmi, které potřebujete.</string>
     <string name="group_link_update_success">Odkaz byl úspěšně aktualizován</string>
-    <string name="no_group_links">Žádné odkazy na skupinu. Odkaz na skupinu vytvoříte kliknutím na tlačítko <i>přidat</i> vpravo dole nebo aktivujte výchozí odkaz v detailním zobrazení skupiny.</string>
+    <string name="no_group_links">Žádné odkazy na skupinu. Odkaz na skupinu vytvoříte kliknutím na tlačítko <i>přidat</i> vpravo dole nebo aktivujte výchozí odkaz v podrobném zobrazení skupiny.</string>
     <string name="group_links_overview_title">Odkazy na skupinu %s</string>
     <string name="group_link_invalid">Expirovaný</string>
     <string name="group_link_valid">Platný</string>
@@ -1319,7 +1319,7 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
     <string name="name_given">Jméno</string>
     <string name="name_family">Příjmení</string>
     <string name="name_prefix">Titul před jménem</string>
-    <string name="name_middle">Další jméno</string>
+    <string name="name_middle">Druhé jméno</string>
     <string name="name_suffix">Titul za jménem</string>
     <string name="full_name">Celé jméno</string>
     <string name="send_contact">Odeslat kontakt</string>
@@ -1330,9 +1330,9 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
     <string name="phoneTypeWork">Práce</string>
     <string name="phoneTypePager">Pager</string>
     <string name="phoneTypeOther">Jiné</string>
-    <string name="phoneTypeCar">Auto</string>
+    <string name="phoneTypeCar">Automobil</string>
     <string name="phoneTypeIsdn">ISDN</string>
-    <string name="phoneTypeOtherFax">Ostatní fax</string>
+    <string name="phoneTypeOtherFax">Jiný fax</string>
     <string name="eventTypeCustom">Vlastní</string>
     <string name="eventTypeBirthday">Narozeniny</string>
     <string name="eventTypeAnniversary">Výročí</string>
@@ -1357,18 +1357,18 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
     <string name="permission_bluetooth_connect_required">Povolte oprávnění „Zařízení v okolí“ k vyhledání připojených Bluetooth náhlavních souprav</string>
     <string name="threema_push_start_webclient_sessions_manually">Threema Push je aktivní. Relace Threema Web a Desktop proto musí být spuštěny manuálně.</string>
     <string name="tooltip_multiple_recipients_title">Více příjemců</string>
-    <string name="tooltip_multiple_recipients_text">Dlouhým klepnutím vyberte více příjemců, kterým poté můžete odeslat stejnou zprávu.</string>
-    <string name="synchronizing">Synchronizace...</string>
+    <string name="tooltip_multiple_recipients_text">Dlouhým klepnutím vyberete více příjemců, kterým poté můžete odeslat stejnou zprávu.</string>
+    <string name="synchronizing">Synchronizace</string>
     <string name="forward_security">Odesílat zprávy s Perfect Forward Secrecy</string>
     <string name="forward_security_mode">Perfect Forward Secrecy</string>
-    <string name="forward_security_mode_none">Žádný</string>
+    <string name="forward_security_mode_none">Ne</string>
     <string name="clear_forward_security">Resetovat Perfect Forward Secrecy pro tento kontakt</string>
     <string name="clear_forward_security_warning">Tuto akci provádějte pouze na pokyn týmu podpory společnosti Threema.</string>
     <string name="forward_security_cleared">Perfect Forward Secrecy relace s tímto kontaktem byly vyčištěny</string>
     <string name="message_without_forward_security">Byla přijata zpráva bez Perfect Forward Secrecy. Ověřte, zda odesílatel záměrně zakázal Perfect Forward Secrecy.</string>
     <string name="forward_security_established">Zprávy v této konverzaci jsou nyní chráněny protokolem Perfect Forward Secrecy.</string>
-    <string name="forward_security_established_rx">Zprávy přijaté v této konverzaci jsou nyní chráněny protokolem Perfect Forward Secrecy. Povolte Perfect Forward Secrecy v kontaktních údajích, aby byly chráněny i odeslané zprávy.</string>
-    <string name="forward_security_reset">Funkce Perfect Forward Secrecy byla resetována. K tomu může dojít, pokud partner v chatu změní své zařízení nebo přeinstaluje aplikaci.</string>
+    <string name="forward_security_established_rx">Zprávy přijaté v této konverzaci jsou nyní chráněny protokolem Perfect Forward Secrecy.</string>
+    <string name="forward_security_reset">Funkce Perfect Forward Secrecy byla resetována. K tomu může dojít, pokud partner v konverzaci změní své zařízení nebo přeinstaluje aplikaci.</string>
     <string name="forward_security_reset_simple">Perfect Forward Secrecy relace s tímto kontaktem byla resetována.</string>
     <string name="forward_security_message_out_of_order">Zpráva byla přijata mimo provoz a nemohla být dešifrována.</string>
     <string name="self_updater_installation_failed">Stažení aktualizace selhalo. Stáhněte a nainstalujte aktualizaci z <a href="https://shop.threema.ch/download">https://shop.threema.ch/download</a> ručně.</string>
@@ -1384,10 +1384,10 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
     <string name="messages_cannot_be_recovered">Zprávy z nich už poté nebudete moci obnovit.</string>
     <string name="contact_deleted">Kontakt odstraněn</string>
     <string name="last_added_contact">Naposledy přidaný</string>
-    <string name="directory_explain_text">Vyhledá v adresáři společnosti jakékoliv zaměstnance, kteří dosud nejsou ve vašem seznamu kontaktů.</string>
+    <string name="directory_explain_text">Vyhledá v adresáři společnosti všechny zaměstnance, kteří dosud nejsou ve vašem seznamu kontaktů.</string>
     <string name="cannot_display_location">Tato poloha nemůže být zobrazena.</string>
     <string name="draw_reply">Nakreslit odpověď</string>
-    <string name="drawing">Výkres</string>
+    <string name="drawing">Malování</string>
     <string name="background_color">Barva pozadí</string>
     <string name="send_video_muted">Odstranit z videa zvuk</string>
     <string name="send_without_audio">Odstranit zvuk</string>
@@ -1402,10 +1402,10 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
     <string name="read_phone_state_short_message">Pro kontrolu probíhajících hovorů udělte aplikaci Threema oprávnění přístupu k telefonu.</string>
     <string name="prefs_title_read_phone_state">Udělit oprávnění Telefon</string>
     <string name="prefs_title_hibernation">Nastavení nepoužívaných aplikací</string>
-    <string name="prefs_summary_hibernation_api_32">Aby se zabránilo tomu, že je aplikace Threema pozastavena systémem po delší neaktivitě, zakažte prosím systémové nastavení „Odstranit oprávnění a uvolnit místo“.</string>
-    <string name="prefs_summary_hibernation_api">Aby se zabránilo tomu, že je aplikace Threema pozastavena systémem po delší neaktivitě, zakažte prosím systémové nastavení „Pozastavit aktivitu při nepouživání“.</string>
+    <string name="prefs_summary_hibernation_api_32">Aby se zabránilo tomu, že aplikace Threema bude pozastavena systémem po delší neaktivitě, zakažte prosím systémové nastavení „Odstranit oprávnění a uvolnit místo“.</string>
+    <string name="prefs_summary_hibernation_api">Aby se zabránilo tomu, že je aplikace Threema pozastavena systémem po delší neaktivitě, zakažte prosím systémové nastavení „Pozastavit aktivitu při nepoužívání“.</string>
     <string name="unable_to_fetch_configuration">Nelze načíst data z konfiguračního serveru. Zkuste to prosím znovu později.</string>
-    <string name="rogue_device_warning">Bylo zjištěno připojení z jiného zařízení s totožným Threema ID. Použili jste v nedávné době své Threema ID na jiném zařízení? &lt;br&gt;&lt;br&gt; Pokud ano, můžete tuto zprávu ignorovat. &lt;br&gt;&lt;br&gt; V opačném případě mohlo dojít k narušení bezpečnosti vašeho soukromého klíče. Zachovejte se dle <a href="https://threema.ch/en/faq/another_connection">našich pokynů</a>, abyste ochránili své zařízení a data, a následně si vytvořte nové ID.</string>
+    <string name="rogue_device_warning">Bylo zjištěno připojení z jiného zařízení s totožným Threema ID. Použili jste v nedávné době své Threema ID na jiném zařízení? &lt;br&gt;&lt;br&gt; Pokud ano, tuto zprávu můžete ignorovat. &lt;br&gt;&lt;br&gt; V opačném případě mohlo dojít k narušení bezpečnosti vašeho soukromého klíče. Zachovejte se dle <a href="https://threema.ch/en/faq/another_connection">našich pokynů</a>, abyste ochránili své zařízení a data, a následně si vytvořte nové ID.</string>
     <string name="fetch2_failure">Synchronizace se zajišťovacím serverem selhala.</string>
     <string name="no_members_support_group_calls">V této skupině nejsou žádní další členové, kteří by mohli přijímat skupinové hovory.</string>
     <string name="group_calls">Skupinové hovory</string>
@@ -1426,29 +1426,29 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
     <string name="fs_key_mismatch">klíče se neshodují</string>
     <string name="tap_to_resend">Klepnutím přepošlete</string>
     <string name="group_calls_tooltip_title">Skupinové hovory</string>
-    <string name="group_calls_tooltip_text">Klepnutím sem pozvete všechny členy skupiny k připojení se k hovoru. Všichni členové skupiny obdrží oznámení a mohou se rozhodnout, zda se chtějí zúčastnit hovoru nebo ne.</string>
+    <string name="group_calls_tooltip_text">Klepnutím sem pozvete všechny členy skupiny k připojení se k hovoru. Všichni členové skupiny obdrží oznámení a mohou se rozhodnout, zda se hovoru zúčastní nebo ne.</string>
     <string name="prefs_title_calls">Zabezpečené hovory</string>
-    <string name="prefs_sum_group_calls_vibration">Vyberte, zda má zařízení vibrovat po zahájení skupinového hovoru</string>
+    <string name="prefs_sum_group_calls_vibration">Vyberte, zda má zařízení po zahájení skupinového hovoru vibrovat</string>
     <string name="forward_security_mode_4dh">kompletní</string>
     <string name="forward_security_mode_2dh">unilaterální</string>
     <string name="forward_security_explanation">Perfect Forward Secrecy (PFS) chrání zaznamenanou komunikaci před zpětným dešifrováním i když je dlouhodobý šifrovací klíč kompromitován.\n\nTato možnost může být povolena v případě, když aplikace na obou stranách podporuje PFS.</string>
     <string name="group_call_inactivity_left">Skupinový hovor byl opuštěn z důvodu neaktivity</string>
     <string name="missing_permission_external_storage">Soubor nelze zkopírovat. Vyzkoušejte následující postup:\n1. Otevřete aplikaci nastavení.\n2. Přejděte do kategorie „Aplikace“ a zvolte položku „Aplikace se speciálním přístupem“.\n3. Klepněte na „Přístup ke všem souborům“.\n4. Klepněte na tři tečky v pravém horním rohu obrazovky a zvolte „Zobrazit systémové aplikace“.\n5. Klepněte na „Externí úložiště“ a ujistěte se, že je povolena volba „Povolit přístup ke správě všech souborů“.\n6. Znovu vyzkoušejte obnovit zálohu.</string>
-    <string name="forward_security_downgraded_status_message">Funkce Perfect Forward Secrecy byla v této konverzaci deaktivována, protože verze aplikace, kterou používá váš partner v chatu, ji nepodporuje.</string>
-    <string name="crop_image_title">Oříznout obr.</string>
-    <string name="scroll_to_bottom">Sjeďte dolů</string>
-    <string name="scroll_to_top">Sjeďte nahoru</string>
-    <string name="contact_sync_mdm_rationale">Váš administrátor povolil synchronizaci kontaktů pro vaše zařízení. Je proto zapotřebí oprávnění pro přístup ke kontaktům.</string>
+    <string name="forward_security_downgraded_status_message">Funkce Perfect Forward Secrecy byla v této konverzaci zakázána, protože verze aplikace, kterou používá váš konverzační partner, ji nepodporuje.</string>
+    <string name="crop_image_title">Oříznout obrázek</string>
+    <string name="scroll_to_bottom">Posunout na konec</string>
+    <string name="scroll_to_top">Posunout na začátek</string>
+    <string name="contact_sync_mdm_rationale">Váš administrátor povolil pro vaše zařízení synchronizaci kontaktů. Je proto zapotřebí udělit oprávnění pro přístup ke kontaktům.</string>
     <string name="welcome_back">Vítejte zpět!</string>
-    <string name="id_restored_successfully">Obnovení vašeho Threema ID proběhlo úspěšně</string>
+    <string name="id_restored_successfully">Obnovení vašeho Threema ID proběhlo úspěšně</string>
     <string name="prefs_dynamic_colors">Dynamická barva</string>
     <string name="prefs_dynamic_colors_sum">Použít systémové barvy</string>
-    <string name="apply_changes">Použit změny</string>
-    <string name="prefs_threema_work_summary">Věděli jste, že Threema také nabízí profesionální komunikační řešení pro firmy? Klepněte sem a zjistěte víc o Threema Work.</string>
+    <string name="apply_changes">Použít změny</string>
+    <string name="prefs_threema_work_summary">Věděli jste, že Threema také nabízí profesionální komunikační řešení pro firmy? Klepněte sem a zjistěte více o Threema Work.</string>
     <string name="notification_channel_voice_message_player">Přehrávač hlasových zpráv</string>
-    <string name="voice_message_from">Hlasová zpráva od: %s</string>
+    <string name="voice_message_from">Hlasová zpráva od %s</string>
     <string name="vm_fg_service_not_allowed">Chyba při přehrávání hlasové zprávy</string>
-    <string name="vm_fg_service_not_allowed_explain">Stiskněte tlačítko pro přehrávání v oznámení s médiem, je-li stále přítomno, nebo otevřete aplikaci a spusťte přehrávání z konverzace.</string>
+    <string name="vm_fg_service_not_allowed_explain">Stiskněte tlačítko pro přehrávání v oznámení s médiem, je‑li stále přítomno, nebo otevřete aplikaci a spusťte přehrávání z konverzace.</string>
     <string name="use_threema_without_this_permission">Používat aplikaci %s bez tohoto oprávnění</string>
     <string name="grant_permission">Udělit oprávnění</string>
     <string name="grant_permission_settings">Udělit oprávnění v nastavení</string>
@@ -1461,7 +1461,7 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
     <string name="permission_enable_in_settings_rationale">Udělte prosím toto oprávnění v nastavení. Vyberte možnost „Oprávnění“ a poté povolte „%s“.</string>
     <string name="group_name">Název skupiny</string>
     <string name="samsung_permission_problem_explain">Aplikace %1$s nemůže přistupovat k mediálním souborům kvůli chybějícímu oprávnění v systémové aplikaci. Chcete‑li tento problém opravit, ujistěte se, že je na následující obrazovce aktivovaný přepínač vedle položky „Externí úložiště“ a restartujte aplikaci %1$s.</string>
-    <string name="min_n_chars">min. %d znaků</string>
+    <string name="min_n_chars">min. %d znaků</string>
     <string name="prefs_title_grant_bluetooth_permission">Udělit oprávnění Bluetooth</string>
     <plurals name="contacts_counter_label">
         <item quantity="few">%d kontakty</item>
@@ -1476,10 +1476,10 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
         <item quantity="other">Skutečně si přejete odstranit %d konverzací?</item>
     </plurals>
     <plurals name="sending_message_failed">
-        <item quantity="few">Nezdařilo se odeslat následující počet zpráv: %1$d</item>
-        <item quantity="many">Nezdařilo se odeslat následující počet zpráv: %1$d</item>
-        <item quantity="one">Nezdařilo se odeslat následující počet zpráv: %1$d</item>
-        <item quantity="other">Nezdařilo se odeslat následující počet zpráv: %1$d</item>
+        <item quantity="few">%1$d zprávy se nezdařilo odeslat</item>
+        <item quantity="many">%1$d zpráv se nezdařilo odeslat</item>
+        <item quantity="one">Jednu zprávu se nezdařilo odeslat</item>
+        <item quantity="other">%1$d zpráv se nezdařilo odeslat</item>
     </plurals>
     <plurals name="selection_counter_label">
         <item quantity="few">Počet vybraných obrázků: %d</item>
@@ -1499,6 +1499,12 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
         <item quantity="one">%d nová zpráva</item>
         <item quantity="other">%d nových zpráv</item>
     </plurals>
+    <plurals name="unread_messages">
+        <item quantity="few">%d nepřečtené zprávy</item>
+        <item quantity="many">%d nepřečtených zpráv</item>
+        <item quantity="one">%d nepřečtená zpráva</item>
+        <item quantity="other">%d nepřečtených zpráv</item>
+    </plurals>
     <plurals name="file_saved">
         <item quantity="few">%d soubory byly úspěšně uloženy.</item>
         <item quantity="many">%d souborů bylo úspěšně uloženo.</item>
@@ -1560,10 +1566,10 @@ přátelům vás automaticky vyhledat, pokud vás mají v adresáři svého tel
         <item quantity="other">Běží %d relací</item>
     </plurals>
     <plurals name="really_delete_outgoing_request">
-        <item quantity="few">Skutečně si přejete odstranit %d požadavky o připojení do skupiny? Mějte na paměti, že požadavky nebudou odvolány a přesto můžete být přijat(a) nebo zamítnut(a), jestliže požadavky dosud nebyly vyřízeny administrátorem skupiny.</item>
-        <item quantity="many">Skutečně si přejete odstranit %d požadavků o připojení do skupiny? Mějte na paměti, že požadavky nebudou odvolány a přesto můžete být přijat(a) nebo zamítnut(a), jestliže požadavky dosud nebyly vyřízeny administrátorem skupiny.</item>
-        <item quantity="one">Skutečně si přejete odstranit %d požadavek o připojení do skupiny? Mějte na paměti, že požadavek nebude odvolán a přesto můžete být přijat(a) nebo zamítnut(a), jestliže požadavek dosud nebyl vyřízen administrátorem skupiny. Totéž platí i pro více požadavků.</item>
-        <item quantity="other">Skutečně si přejete odstranit %d požadavků o připojení do skupiny? Mějte na paměti, že požadavky nebudou odvolány a přesto můžete být přijat(a) nebo zamítnut(a), jestliže požadavky dosud nebyly vyřízeny administrátorem skupiny.</item>
+        <item quantity="few">Skutečně si přejete odstranit %d požadavky o připojení do skupiny? Mějte na paměti, že požadavky nebudou odvolány a přesto můžete být přijat(a) nebo zamítnut(a), jestliže požadavky dosud nebyly vyřízeny.</item>
+        <item quantity="many">Skutečně si přejete odstranit %d požadavků o připojení do skupiny? Mějte na paměti, že požadavky nebudou odvolány a přesto můžete být přijat(a) nebo zamítnut(a), jestliže požadavky dosud nebyly vyřízeny.</item>
+        <item quantity="one">Skutečně si přejete odstranit %d požadavek o připojení do skupiny? Mějte na paměti, že požadavek nebude odvolán a přesto můžete být přijat(a) nebo zamítnut(a), jestliže požadavek dosud nebyl vyřízen.</item>
+        <item quantity="other">Skutečně si přejete odstranit %d požadavků o připojení do skupiny? Mějte na paměti, že požadavky nebudou odvolány a přesto můžete být přijat(a) nebo zamítnut(a), jestliže požadavky dosud nebyly vyřízeny.</item>
     </plurals>
     <plurals name="really_delete_incoming_request">
         <item quantity="few">Skutečně si přejete odstranit %d požadavky o připojení do skupiny? Na požadavky po jejich smazání nebude možné odpovědět.</item>

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

@@ -619,7 +619,7 @@ sicheren Ort gesichert oder ausgedruckt haben.</string>
 	<string name="prefs_sum_passphrase">Passphrase für lokale Verschlüsselung verlangen</string>
 	<string name="prefs_title_masterkey_change_passphrase">Passphrase ändern</string>
 	<string name="storage_total">Interner Speicherplatz</string>
-	<string name="storage_threema">Von Threema genutzt</string>
+	<string name="storage_threema">Von %s genutzt</string>
 	<string name="storage_total_free">Freier Speicher</string>
 	<string name="storage_total_in_use">In Benutzung</string>
 	<string name="one_year">1 Jahr</string>
@@ -1461,7 +1461,7 @@ sicheren Ort gesichert oder ausgedruckt haben.</string>
 	<string name="forward_security_cleared">Perfect Forward Secrecy-Sitzungen mit diesem Kontakt wurden gelöscht</string>
 	<string name="message_without_forward_security">Eine Nachricht ohne Perfect Forward Secrecy wurde empfangen. Stellen Sie sicher, dass der Absender absichtlich Perfect Forward Secrecy deaktiviert hat.</string>
 	<string name="forward_security_established">Nachrichten in diesem Chat sind nun durch Perfect Forward Secrecy geschützt.</string>
-	<string name="forward_security_established_rx">Nachrichten, die in diesem Chat empfangen werden, sind nun durch Perfect Forward Secrecy geschützt. Aktivieren Sie Perfect Forward Secrecy in den Kontaktdetails, um auch gesendete Nachrichten zu schützen.</string>
+	<string name="forward_security_established_rx">Nachrichten, die in diesem Chat empfangen werden, sind nun durch Perfect Forward Secrecy geschützt.</string>
 	<string name="forward_security_reset">Die Perfect Forward Secrecy-Sitzung wurde zurückgesetzt. Dies kann geschehen, wenn der Chat-Partner ein neues Gerät verwendet oder die App neu installiert.</string>
 	<string name="forward_security_reset_simple">Die Perfect Forward Secrecy-Sitzung mit diesem Kontakt wurde zurückgesetzt.</string>
 	<string name="forward_security_message_out_of_order">Eine Nachricht wurde in falscher Reihenfolge empfangen und konnte nicht entschlüsselt werden.</string>
@@ -1484,7 +1484,7 @@ sicheren Ort gesichert oder ausgedruckt haben.</string>
 	<string name="messages_cannot_be_recovered">Die Nachrichten können nicht wiederhergestellt werden.</string>
 	<string name="contact_deleted">Kontakt wurde gelöscht</string>
 	<string name="last_added_contact">Zuletzt hinzugefügt</string>
-	<string name="directory_explain_text">Suchen Sie im Firmenverzeichnis nach beliebigen Mitarbeitern, die sich noch nicht in Ihrer Kontaktliste befinden.</string>
+	<string name="directory_explain_text">Suchen Sie im Unternehmensverzeichnis nach beliebigen Mitarbeitern, die sich noch nicht in Ihrer Kontaktliste befinden.</string>
 	<string name="cannot_display_location">Dieser Ort kann nicht angezeigt werden.</string>
 	<string name="draw_reply">Zeichnungsantwort</string>
 	<string name="drawing">Zeichnung</string>
@@ -1568,7 +1568,7 @@ sicheren Ort gesichert oder ausgedruckt haben.</string>
 	<string name="permission_camera">Kamera</string>
 	<string name="permission_microphone">Mikrofon</string>
 	<string name="permission_read_phone_state">Telefon</string>
-	<string name="permission_enable_in_settings_rationale">Bitte erlauben Sie die Berechtigung in den Einstellungen. Wählen Sie «Berechtigungen» aus und lassen Sie danach “%s” zu.</string>
+	<string name="permission_enable_in_settings_rationale">Bitte erlauben Sie die Berechtigung in den Einstellungen. Wählen Sie «Berechtigungen» aus und lassen Sie danach «%s» zu.</string>
     <string name="group_name">Name der Gruppe</string>
 	<string name="samsung_permission_problem_explain">%1$s hat keinen Zugriff auf Mediendaten aufgrund der fehlenden Berechtigung einer System-App. Um das Problem zu beheben, stellen Sie sicher, dass der Schalter neben «Externer Speicher» im folgenden Bildschirm aktiviert ist, und starten Sie %1$s neu.</string>
 	<string name="min_n_chars">min. %d Zeichen</string>

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

@@ -543,7 +543,7 @@ Introduzca una pregunta para su sondeo.</string>
     <string name="prefs_sum_passphrase">Requerir frase de seguridad para desbloquear cifrado local</string>
     <string name="prefs_title_masterkey_change_passphrase">Cambiar frase de seguridad</string>
     <string name="storage_total">Espacio de almacenamiento interno</string>
-    <string name="storage_threema">Utilizado por Threema</string>
+    <string name="storage_threema">Utilizado por %s</string>
     <string name="storage_total_free">Espacio libre total</string>
     <string name="storage_total_in_use">En uso</string>
     <string name="one_year">1 año</string>
@@ -681,7 +681,7 @@ almacenado.</string>
     <string name="action_clone_group">Clonar grupo</string>
     <string name="clone_group_message">Se creará un clon del grupo contigo como admiistrador. ¿Continuar?</string>
     <string name="prefs_proximity_sensor">Utilizar sensor de proximidad</string>
-    <string name="prefs_proximity_sensor_explain">Utilziar auricular para reproducir mensajes de voz si el sensor de proximidad está tapado</string>
+    <string name="prefs_proximity_sensor_explain">Utilizar auricular para reproducir mensajes de voz si el sensor de proximidad está tapado</string>
     <string name="error_creating_group">Error al crear el grupo</string>
     <string name="no_media_found_generic">No hay elementos multimedia en este chat que coincidan con la selección.</string>
     <string name="max_images_reached">Se pueden editar un máximo de %d elementos a la vez.</string>
@@ -1374,7 +1374,7 @@ almacenado.</string>
     <string name="forward_security_cleared">Se han borrado las sesiones Perfect Forward Secrecy con este contacto</string>
     <string name="message_without_forward_security">Se ha recibido un mensaje sin Perfect Forward Secrecy. Comprueba que el remitente ha desactivado el Perfect Forward Secrecy intencionadamente.</string>
     <string name="forward_security_established">Los mensajes de este chat están protegidos con Perfect Forward Secrecy.</string>
-    <string name="forward_security_established_rx">Los mensajes recibidos en este chat están protegidos con Perfect Forward Secrecy. Activa Perfect Forward Secrecy en los datos de los contactos para proteger también los mensajes enviados.</string>
+    <string name="forward_security_established_rx">Los mensajes recibidos en este chat están protegidos con Perfect Forward Secrecy.</string>
     <string name="forward_security_reset">Se ha restablecido la sesión de Perfect Forward Secrecy. Esto puede ocurrir si el compañero de chat cambia de dispositivo o reinstala la app.</string>
     <string name="forward_security_reset_simple">Se ha restablecido la sesión Perfect Forward Secrecy con este contacto.</string>
     <string name="forward_security_message_out_of_order">Se ha recibido un mensaje inadecuado y no se ha podido descifrar.</string>

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

@@ -541,7 +541,7 @@ Veuillez saisir une question pour votre enquête.</string>
     <string name="prefs_sum_passphrase">Nécessite une phrase secrète pour débloquer le chiffrement local</string>
     <string name="prefs_title_masterkey_change_passphrase">Changer la phrase secrète</string>
     <string name="storage_total">Espace sur le stockage interne</string>
-    <string name="storage_threema">Utilisé par Threema</string>
+    <string name="storage_threema">Utilisé par %s</string>
     <string name="storage_total_free">Espace libre total</string>
     <string name="storage_total_in_use">Utilisé</string>
     <string name="one_year">1 an</string>
@@ -1365,7 +1365,7 @@ Veuillez saisir une question pour votre enquête.</string>
     <string name="forward_security_cleared">Les sessions de confidentialité persistante avec ce contact ont été effacées</string>
     <string name="message_without_forward_security">Un message sans confidentialité persistante a été reçu. Vérifiez que l\'émetteur a délibérément désactivé la confidentialité persistante.</string>
     <string name="forward_security_established">Les messages de ce chat sont maintenant protégés par la confidentialité persistante.</string>
-    <string name="forward_security_established_rx">Les messages reçus dans ce chat sont maintenant protégés par la confidentialité persistante. Activez la confidentialité persistante dans les détails du contact pour protéger également les messages envoyés.</string>
+    <string name="forward_security_established_rx">Les messages reçus dans ce chat sont maintenant protégés par la confidentialité persistante.</string>
     <string name="forward_security_reset">La session de confidentialité persistante a été réinitialisée. Cela peut se produire lorsque le partenaire de discussion change d\'appareil ou réinstalle l\'application.</string>
     <string name="forward_security_reset_simple">La session de confidentialité persistante avec ce contact a été réinitialisée.</string>
     <string name="forward_security_message_out_of_order">Un message a été reçu en désordre et n\'a pas pu être déchiffré.</string>

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

@@ -542,7 +542,7 @@
     <string name="prefs_sum_passphrase">Jelszó megkövetelése a helyi titkosításhoz</string>
     <string name="prefs_title_masterkey_change_passphrase">Jelszó módosítása</string>
     <string name="storage_total">Belső tároló</string>
-    <string name="storage_threema">Threema által használt</string>
+    <string name="storage_threema">%s által használt</string>
     <string name="storage_total_free">Szabad tárhely</string>
     <string name="storage_total_in_use">Felhasznált</string>
     <string name="one_year">1 év</string>

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

@@ -561,7 +561,7 @@ automaticamente in caso di inattività dopo un intervallo predefinito (solo cara
     <string name="prefs_sum_passphrase">È richiesta una passphrase per sbloccare il criptaggio locale</string>
     <string name="prefs_title_masterkey_change_passphrase">Cambia passphrase</string>
     <string name="storage_total">"Spazio nella memoria interna "</string>
-    <string name="storage_threema">Usato da Threema</string>
+    <string name="storage_threema">Usato da %s</string>
     <string name="storage_total_free">Spazio libero totale</string>
     <string name="storage_total_in_use">In uso</string>
     <string name="one_year">1 anno</string>
@@ -1376,7 +1376,7 @@ automaticamente in caso di inattività dopo un intervallo predefinito (solo cara
     <string name="forward_security_cleared">Le sessioni Perfect Forward Secrecy con questo contatto sono state cancellate</string>
     <string name="message_without_forward_security">È stato ricevuto un messaggio senza Perfect Forward Secrecy. Assicurati che il mittente abbia volutamente disattivato il Perfect Forward Secrecy.</string>
     <string name="forward_security_established">I messaggi di questa chat sono d\'ora in poi protetti da Perfect Forward Secrecy.</string>
-    <string name="forward_security_established_rx">I messaggi ricevuti in questa chat sono d\'ora in poi protetti da Perfect Forward Secrecy. Attiva Perfect Forward Secrecy nei dettagli contatto per proteggere anche i messaggi inviati.</string>
+    <string name="forward_security_established_rx">I messaggi ricevuti in questa chat sono d\'ora in poi protetti da Perfect Forward Secrecy.</string>
     <string name="forward_security_reset">La sessione Perfect Forward Secrecy con questo contatto è stata reimpostata. Ciò può succedere se il contatto ha cambiato dispositivo o reinstallato l\'app.</string>
     <string name="forward_security_reset_simple">La sessione di Perfect Forward Secrecy con questo contatto è stata reimpostata.</string>
     <string name="forward_security_message_out_of_order">Un messaggio è stato ricevuto nell\'ordine sbagliato e quindi non è potuto essere criptato.</string>

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

@@ -548,7 +548,7 @@ https://myid.threema.ch/revoke に入力することで ID を削除すること
     <string name="prefs_sum_passphrase">端末に保存されるデータの復号化にパスフレーズを要求します</string>
     <string name="prefs_title_masterkey_change_passphrase">パスフレーズを変更</string>
     <string name="storage_total">内部ストレージの容量</string>
-    <string name="storage_threema">Threemaで使用</string>
+    <string name="storage_threema">%sで使用</string>
     <string name="storage_total_free">ストレージの空き容量</string>
     <string name="storage_total_in_use">使用中</string>
     <string name="one_year">1年</string>
@@ -1365,7 +1365,7 @@ https://myid.threema.ch/revoke に入力することで ID を削除すること
     <string name="forward_security_cleared">この連絡先とのPerfect Forward Secrecyセッションはクリアされました</string>
     <string name="message_without_forward_security">Perfect Forward Secrecyを使用しないメッセージを受信しました。送信者がPerfect Forward Secrecyを意図的に無効にしていることを確認してください。</string>
     <string name="forward_security_established">このトークのメッセージはPerfect Forward Secrecyによって保護されるようになりました。</string>
-    <string name="forward_security_established_rx">このトークで受信したメッセージはPerfect Forward Secrecyによって保護されるようになりました。送信するメッセージも保護するには、連絡先の詳細でPerfect Forward Secrecyを有効にしてください。</string>
+    <string name="forward_security_established_rx">このトークで受信したメッセージはPerfect Forward Secrecyによって保護されるようになりました。</string>
     <string name="forward_security_reset">この連絡先とのPerfect Forward Secrecyセッションはリセットされました。その連絡先がデバイスを変更したか、アプリを再インストールした可能性があります。</string>
     <string name="forward_security_reset_simple">この連絡先とのPerfect Forward Secrecyセッションはリセットされました。</string>
     <string name="forward_security_message_out_of_order">メッセージが間違った順序で受信されたため、復号化できませんでした。</string>

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

@@ -25,8 +25,8 @@
 
 	<color name="mention_background">@color/material_grey_550</color>
 	<color name="mention_background_inverted">@color/material_grey_400</color>
-	<color name="mention_text_color">@color/md_theme_light_onBackground</color>
-	<color name="mention_text_color_inverted">@color/md_theme_light_background</color>
+	<color name="mention_text_color">@color/md_theme_light_background</color>
+	<color name="mention_text_color_inverted">@color/md_theme_light_onBackground</color>
 
 	<color name="voice_recorder_counter_background">@color/dark_surface</color>
 </resources>

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

@@ -544,7 +544,7 @@ Voer een vraag in voor uw poll.</string>
     <string name="prefs_sum_passphrase">Wachtwoordzin nodig om lokale versleuteling vrij te geven</string>
     <string name="prefs_title_masterkey_change_passphrase">Wachtwoordzin wijzigen</string>
     <string name="storage_total">Ruimte op interne opslag</string>
-    <string name="storage_threema">Gebruikt door Threema</string>
+    <string name="storage_threema">Gebruikt door %s</string>
     <string name="storage_total_free">Totale vrije ruimte</string>
     <string name="storage_total_in_use">In gebruik</string>
     <string name="one_year">1 jaar</string>
@@ -1368,7 +1368,7 @@ Weet u zeker dat u Threema anoniem wil gebruiken?</string>
     <string name="forward_security_cleared">De Perfect Forward Secrecy-sessies met deze contactpersoon zijn gewist</string>
     <string name="message_without_forward_security">Er is een bericht zonder Perfect Forward Secrecy ontvangen. Controleer of de afzender Perfect Forward Secrecy opzettelijk heeft uitgeschakeld.</string>
     <string name="forward_security_established">De berichten in deze chat zijn nu beschermd met Perfect Forward Secrecy.</string>
-    <string name="forward_security_established_rx">De ontvangen berichten in deze chat zijn nu beschermd met Perfect Forward Secrecy. Door Perfect Forward Secrecy in de contactgegevens in te schakelen, zijn verzonden berichten ook beschermd.</string>
+    <string name="forward_security_established_rx">De ontvangen berichten in deze chat zijn nu beschermd met Perfect Forward Secrecy.</string>
     <string name="forward_security_reset">De Perfect Forward Secrecy-sessie is gereset. Dit kan gebeuren wanneer de gesprekspartner een ander apparaat gebruikt of de app opnieuw installeert.</string>
     <string name="forward_security_reset_simple">De Perfect Forward Secrecy-sessie met deze contactpersoon is gereset.</string>
     <string name="forward_security_message_out_of_order">Er is een bericht buiten de volgorde ontvangen dat niet is versleuteld.</string>

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

@@ -540,7 +540,7 @@ http://www.7-zip.org eller https://itunes.apple.com/us/app/the-unarchiver/id4254
     <string name="prefs_sum_passphrase">Krever en passfrase for å låse opp lokal kryptering</string>
     <string name="prefs_title_masterkey_change_passphrase">Endre passfrase</string>
     <string name="storage_total">Lagringsplass på intern lagring</string>
-    <string name="storage_threema">I bruk av Threema</string>
+    <string name="storage_threema">I bruk av %s</string>
     <string name="storage_total_free">Tilgjengelig plass</string>
     <string name="storage_total_in_use">I bruk</string>
     <string name="one_year">1 år</string>
@@ -1371,7 +1371,7 @@ Om du bytter til en ny enhet, vennligst avinstaller eller deaktiver %s på den g
     <string name="forward_security_cleared">Perfect Forward Secrecy-økt med denne kontakten har blitt fjernet</string>
     <string name="message_without_forward_security">En melding uten Perfect Forward Secrecy er mottatt. Verifiser at avsender har skrudd av Perfect Forward Secrecy med vilje.</string>
     <string name="forward_security_established">Meldinger i denne samtalen er nå beskyttet av Perfect Forward Secrecy.</string>
-    <string name="forward_security_established_rx">Meldinger mottatt i denne samtalen blir nå beskyttet av Perfect Forward Secrecy. Skru på Perfect Forward Secrecy i kontaktdetaljer for å beskytte sendte medlinger også.</string>
+    <string name="forward_security_established_rx">Meldinger mottatt i denne samtalen blir nå beskyttet av Perfect Forward Secrecy.</string>
     <string name="forward_security_reset">Perfect Forward Secrecy-økten har blitt resatt. Dette kan skje om den du chattet med har byttet enhet eller reinstallert appen.</string>
     <string name="forward_security_reset_simple">Økten med Perfect Forward Secrecy som du har med denne kontakten har blitt resatt.</string>
     <string name="forward_security_message_out_of_order">En melding ble mottatt ute av rekkefølge og kunne derfor ikke bli dekryptert.</string>

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

@@ -542,7 +542,7 @@ Wprowadź pytanie do swojej ankiety.</string>
     <string name="prefs_sum_passphrase">Wymagaj hasła w celu odblokowania lokalnego szyfrowania</string>
     <string name="prefs_title_masterkey_change_passphrase">Zmień hasło</string>
     <string name="storage_total">Przestrzeń w pamięci wewnętrznej</string>
-    <string name="storage_threema">Wykorzystana przez Threema</string>
+    <string name="storage_threema">Wykorzystana przez %s</string>
     <string name="storage_total_free">Wolna przestrzeń łącznie</string>
     <string name="storage_total_in_use">Wykorzystana</string>
     <string name="one_year">1 rok</string>
@@ -1380,7 +1380,7 @@ anonimowo?</string>
     <string name="forward_security_cleared">Sesje Perfect Forward Secrecy z tym kontaktem zostały wyczyszczone</string>
     <string name="message_without_forward_security">Odebrano wiadomość bez Perfect Forward Secrecy. Sprawdź, że nadawca rozmyślnie wyłączył Perfect Forward Secrecy.</string>
     <string name="forward_security_established">Wiadomości w tym czacie są teraz chronione przez Perfect Forward Secrecy.</string>
-    <string name="forward_security_established_rx">Wiadomości odbierane w tym czacie są teraz chronione przez Perfect Forward Secrecy. Włącz Perfect Forward Secrecy w danych kontaktu, aby chronić również wysyłane wiadomości.</string>
+    <string name="forward_security_established_rx">Wiadomości odbierane w tym czacie są teraz chronione przez Perfect Forward Secrecy.</string>
     <string name="forward_security_reset">Zresetowano sesję Perfect Forward Secrecy. Może się to zdarzyć, gdy partner na czacie zmienił urządzenia lub ponownie zainstalował aplikację.</string>
     <string name="forward_security_reset_simple">Zresetowano sesję Perfect Forward Secrecy z tym kontaktem.</string>
     <string name="forward_security_message_out_of_order">Wiadomość została odebrana poza kolejnością i nie można jej odszyfrować.</string>

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

@@ -541,7 +541,7 @@ Por favor, insira uma pergunta para a sua enquete.</string>
     <string name="prefs_sum_passphrase">Exige uma frase secreta para desbloquear criptografia local</string>
     <string name="prefs_title_masterkey_change_passphrase">Alterar frase secreta</string>
     <string name="storage_total">Espaço em armazenamento interno</string>
-    <string name="storage_threema">Usao pelo Threema</string>
+    <string name="storage_threema">Usao pelo %s</string>
     <string name="storage_total_free">Espaço livre total</string>
     <string name="storage_total_in_use">Em uso</string>
     <string name="one_year">1 ano</string>
@@ -1365,7 +1365,7 @@ Por favor, insira uma pergunta para a sua enquete.</string>
     <string name="forward_security_cleared">As sessões com Perfect Forward Secrecy deste contato foram apagadas</string>
     <string name="message_without_forward_security">Uma mensagem sem Perfect Forward Secrecy foi recebida. Verifique se o remetente desativou o FPS intencionalmente.</string>
     <string name="forward_security_established">As mensagens nesta conversa estão protegidas pelo Perfect Forward Secrecy.</string>
-    <string name="forward_security_established_rx">As mensagens recebidas nesta conversa estão protegidas pelo Perfect Forward Secrecy (PFS). Ative o PFS nos detalhes de contato para proteger também as mensagens enviadas.</string>
+    <string name="forward_security_established_rx">As mensagens recebidas nesta conversa estão protegidas pelo Perfect Forward Secrecy (PFS).</string>
     <string name="forward_security_reset">A sessão com Perfect Forward Secrecy foi redefinida. Isso pode acontecer quando o contato do chat muda de dispositivo ou reinstala o app.</string>
     <string name="forward_security_reset_simple">A sessão com Perfect Forward Secrecy deste contato foi redefinida.</string>
     <string name="forward_security_message_out_of_order">Uma mensagem foi recebida incorretamente e não pôde ser descodificada.</string>

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

@@ -526,7 +526,7 @@ Endatescha in pled-clav per tes backup da datas.</string>
     <string name="prefs_sum_passphrase">Pretender ina frasa secreta per la codaziun locala</string>
     <string name="prefs_title_masterkey_change_passphrase">Midar la frasa secreta</string>
     <string name="storage_total">Capacitad da memorisar interna</string>
-    <string name="storage_threema">Utilisà per Threema</string>
+    <string name="storage_threema">Utilisà per %s</string>
     <string name="storage_total_free">Arcun liber</string>
     <string name="storage_total_in_use">Vegn utilisà</string>
     <string name="one_year">1 onn</string>

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

@@ -540,7 +540,7 @@
     <string name="prefs_sum_passphrase">Требовать кодовую фразу для разблокирования локального шифрования</string>
     <string name="prefs_title_masterkey_change_passphrase">Изменить кодовую фразу</string>
     <string name="storage_total">Место на внутренней памяти</string>
-    <string name="storage_threema">Используется Threema</string>
+    <string name="storage_threema">Используется %s</string>
     <string name="storage_total_free">Всего свободного места</string>
     <string name="storage_total_in_use">Используется</string>
     <string name="one_year">1 года</string>
@@ -1364,7 +1364,7 @@
     <string name="forward_security_cleared">Сессии Perfect Forward Secrecy с этим контактом очищены</string>
     <string name="message_without_forward_security">Получено сообщение, в котором не использовался протокол Perfect Forward Secrecy. Убедитесь, что отправитель намеренно отключил Perfect Forward Secrecy.</string>
     <string name="forward_security_established">Сообщения в этом чате теперь защищены протоколом Perfect Forward Secrecy.</string>
-    <string name="forward_security_established_rx">Сообщения, получаемые в этом чате, теперь защищены протоколом Perfect Forward Secrecy. Включите Perfect Forward Secrecy в сведениях о контакте, чтобы защитить также отправляемые сообщения.</string>
+    <string name="forward_security_established_rx">Сообщения, получаемые в этом чате, теперь защищены протоколом Perfect Forward Secrecy.</string>
     <string name="forward_security_reset">Сессия Perfect Forward Secrecy сброшена. Это может произойти, если собеседник в чате переходит на другое устройство или переустанавливает приложение.</string>
     <string name="forward_security_reset_simple">Сессия Perfect Forward Secrecy с этим контактом сброшена.</string>
     <string name="forward_security_message_out_of_order">Получено сообщение, которое невозможно расшифровать.</string>

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

@@ -541,7 +541,7 @@ https://myid.threema.ch/revoke v prípade, že ho stratíte alebo vám bude odcu
     <string name="prefs_sum_passphrase">Na odomknutie lokálneho úložiska sa vyžaduje heslo</string>
     <string name="prefs_title_masterkey_change_passphrase">Zmeniť heslo</string>
     <string name="storage_total">Miesto na ínternom úložisku</string>
-    <string name="storage_threema">Využité Threemou</string>
+    <string name="storage_threema">Využité %s</string>
     <string name="storage_total_free">Volné miesto</string>
     <string name="storage_total_in_use">Využité</string>
     <string name="one_year">1 rok</string>
@@ -1366,7 +1366,7 @@ Vykonajte prosím zálohú vašich údajov vhodnou metódou.</string>
     <string name="forward_security_cleared">Relácie Perfect Forward Secrecy s týmto kontaktom boli odstránené</string>
     <string name="message_without_forward_security">Bola prijatá správa bez Perfect Forward Secrecy. Overte, či odosielateľ úmyselne zakázal funkciu Perfect Forward Secrecy.</string>
     <string name="forward_security_established">Správy v tomto rozhovore sú teraz chránené systémom Perfect Forward Secrecy.</string>
-    <string name="forward_security_established_rx">Správy prijaté v tejto konverzácii sú odteraz chránené systémom Perfect Forward Secrecy (PFS). Povoľte PFS po kliknútí na ikonu kontaktu, aby ste zabezpečili aj odosielané správy.</string>
+    <string name="forward_security_established_rx">Správy prijaté v tejto konverzácii sú odteraz chránené systémom Perfect Forward Secrecy (PFS).</string>
     <string name="forward_security_reset">Relácia Perfect Forward Secrecy bola resetovaná. Môže sa to stať, keď partner rozhovoru zmení zariadenie alebo preinštaluje aplikáciu.</string>
     <string name="forward_security_reset_simple">Relácia Perfect Forward Secrecy s týmto kontaktom bola resetovaná.</string>
     <string name="forward_security_message_out_of_order">Správa bola prijatá ako nefunkčná a nedá sa dešifrovať.</string>
@@ -1546,6 +1546,16 @@ Vykonajte prosím zálohú vašich údajov vhodnou metódou.</string>
         <item quantity="one">%d spustená relácia</item>
         <item quantity="other">%d spustených relácií</item>
     </plurals>
+    <plurals name="really_delete_outgoing_request">
+        <item quantity="few">Naozaj chcete odstrániť %d skupinové žiadosti?
+    Uvedomte si, že žiadosti nebudú odvolané a stále môžete byť akceptovaní alebo vyžiadaní,
+    ak žiadosti ešte neboli zodpovedané.</item>
+        <item quantity="many">Naozaj chcete odstrániť %d skupinových žiadostí?
+    Uvedomte si, že žiadosti nebudú odvolané a stále môžete byť akceptovaní alebo vyžiadaní,
+    ak žiadosti ešte neboli zodpovedané.</item>
+        <item quantity="one">Naozaj chcete odstrániť %d skupinovú žiadosť? Uvedomte si, že žiadosť nebude odvolaná a ak na žiadosť ešte nebolo odpovedané, môžete byť stále prijatý alebo vyžiadaný.</item>
+        <item quantity="other">Naozaj chcete odstrániť %d skupinových žiadostí? Uvedomte si, že žiadosti nebudú odvolané a stále môžete byť akceptovaní alebo vyžiadaní, ak žiadosti ešte neboli zodpovedané.</item>
+    </plurals>
     <plurals name="really_delete_incoming_request">
         <item quantity="few">Naozaj chcete odstrániť %d skupinové žiadosťi? Po odstránení nemôžete na žiadosti odpovedať.</item>
         <item quantity="many">Naozaj chcete odstrániť %d skupinových žiadosťí? Po odstránení nemôžete na žiadosti odpovedať.</item>

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

@@ -539,7 +539,7 @@ tekrar denemeden önce girdiğiniz numaranın doğru olduğundan ve mobil ağa b
     <string name="prefs_sum_passphrase">Yerel şifrelemenin kilidini açmak için bir parola iste</string>
     <string name="prefs_title_masterkey_change_passphrase">Parolayı değiştir</string>
     <string name="storage_total">Dahili depolama alanı</string>
-    <string name="storage_threema">Threema tarafından kullanılan</string>
+    <string name="storage_threema">%s tarafından kullanılan</string>
     <string name="storage_total_free">Toplam boş alan</string>
     <string name="storage_total_in_use">Kullanımda</string>
     <string name="one_year">1 yıl</string>
@@ -1361,7 +1361,7 @@ sunucularımıza güvenli bir şekilde iletildi. Kişisel anahtar hiçbir zaman
     <string name="forward_security_cleared">Bu kişiyle Mükemmel İletme Gizliliği oturumları temizlendi</string>
     <string name="message_without_forward_security">Mükemmel İletme Gizliliği olmayan bir mesaj alındı. Gönderenin Mükemmel İletme Gizliliğini kasıtlı olarak devre dışı bıraktığını doğrulayın.</string>
     <string name="forward_security_established">Bu sohbetteki mesajlar artık Mükemmel İletme Gizliliği ile korunmaktadır.</string>
-    <string name="forward_security_established_rx">Bu sohbette alınan mesajlar artık Mükemmel İletme Gizliliği ile korunmaktadır. Gönderilen mesajları da korumak için iletişim ayrıntılarında Mükemmel İletme Gizliliğini etkinleştirin.</string>
+    <string name="forward_security_established_rx">Bu sohbette alınan mesajlar artık Mükemmel İletme Gizliliği ile korunmaktadır.</string>
     <string name="forward_security_reset">Mükemmel İletme Gizliliği oturumu sıfırlandı. Bu, sohbet ortağı cihazları değiştirdiğinde veya uygulamayı yeniden yüklediğinde meydana gelebilir.</string>
     <string name="forward_security_reset_simple">Bu kişiyle Mükemmel İletme Gizliliği oturumu sıfırlandı.</string>
     <string name="forward_security_message_out_of_order">Düzensiz bir mesaj alındı ​​ve şifresi çözülemedi.</string>

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

@@ -217,8 +217,8 @@
     <string name="prefs_header_other">Інше</string>
     <string name="an_error_occurred">Сталася помилка</string>
     <string name="an_error_occurred_more">Сталася помилка: \"%1$s\"</string>
-    <string name="acknowledge">Подобається</string>
-    <string name="decline">Не подобається</string>
+    <string name="acknowledge">Погоджуюся</string>
+    <string name="decline">Не погоджуюся</string>
     <string name="forward_message">Переслати повідомлення</string>
     <string name="file_is_not_a_image">Вибраний файл не є зображенням</string>
     <string name="connection_error">Не вдалося підключитися. Повторіть спробу пізніше.</string>
@@ -310,8 +310,8 @@
     <string name="backup_delete_confirm">Файл резервної копії видалено</string>
     <string name="message_log_title">Про повідомлення</string>
     <string name="state_read">прочитано</string>
-    <string name="state_ack">подобається</string>
-    <string name="state_dec">не подобається</string>
+    <string name="state_ack">погоджуюся</string>
+    <string name="state_dec">не погоджуюся</string>
     <string name="state_delivered">доставлено</string>
     <string name="state_sending">надсилання</string>
     <string name="state_pending">очікування</string>
@@ -492,7 +492,7 @@
     <string name="back">Назад</string>
     <string name="wearable_reply">Відповісти</string>
     <string name="wearable_reply_label">Відповісти \"%s\"</string>
-    <string name="message_acknowledged">\"Подобається\" надіслано</string>
+    <string name="message_acknowledged">\"Погоджуюся\" надіслано</string>
     <string name="push_disable_text">Якщо продовжити, замість системного сервісу push-сповіщень буде використовуватися сервіс Threema Push. Він допомагає зменшити цифровий слід, але для його роботи потрібно дозволити додатку працювати у фоновому режимі. Якщо ви не знайшли, як це зробити, у налаштуваннях системи, зверніться за допомогою до служби підтримки виробника пристрою.</string>
     <string name="ballot_intermediate_results_show">Показувати попередні результати</string>
     <string name="converting_video">Обробка відео</string>
@@ -562,7 +562,7 @@
     <string name="prefs_sum_passphrase">Запитувати парольну фразу для розшифрування локальних даних</string>
     <string name="prefs_title_masterkey_change_passphrase">Змінити парольну фразу</string>
     <string name="storage_total">Об\'єм внутрішньої пам\'яті</string>
-    <string name="storage_threema">Використовується Threema</string>
+    <string name="storage_threema">Використовується %s</string>
     <string name="storage_total_free">Вільно</string>
     <string name="storage_total_in_use">Зайнято</string>
     <string name="one_year">1 рік</string>
@@ -616,7 +616,7 @@
     <string name="permission_storage_required">Щоб зберігати та надсилати медіафайли, надайте Threema доступ до сховища.</string>
     <string name="permission_location_required">Щоб надіслати розташування, надайте Threema доступ до геоданих.</string>
     <string name="permission_contacts_required">Щоб надіслати контакти, надайте Threema доступ до них.</string>
-    <string name="message_declined">\"Не подобається\" надіслано</string>
+    <string name="message_declined">\"Не погоджуюся\" надіслано</string>
     <string name="notifications_settings">Налаштування сповіщень</string>
     <string name="notifications_default">Налаштування за умовчанням</string>
     <string name="notifications_until">До %s</string>
@@ -1599,13 +1599,13 @@
     </plurals>
     <plurals name="really_delete_outgoing_request">
         <item quantity="few">Справді видалити %d запити на приєднання до групи?
-			Зауважте, що запити не буде відкликано і вас можуть усе одно прийняти до групи,
+			Зауважте, що запити не буде відкликано і вас можуть усе одно прийняти або запросити до групи,
 			якщо на запити ще не відповіли.</item>
         <item quantity="many">Справді видалити %d запитів на приєднання до групи?
-			Зауважте, що запити не буде відкликано і вас можуть усе одно прийняти до групи,
+			Зауважте, що запити не буде відкликано і вас можуть усе одно прийняти або запросити до групи,
 			якщо на запити ще не відповіли.</item>
-        <item quantity="one">Справді видалити %d запит на приєднання до групи? Зауважте, що запит не буде відкликано і вас можуть усе одно прийняти до групи, якщо на запит ще не відповіли.</item>
-        <item quantity="other">Справді видалити %d запита на приєднання до групи? Зауважте, що запити не буде відкликано і вас можуть усе одно прийняти до групи, якщо на запити ще не відповіли.</item>
+        <item quantity="one">Справді видалити %d запит на приєднання до групи? Зауважте, що запит не буде відкликано і вас можуть усе одно прийняти або запросити до групи, якщо на запит ще не відповіли.</item>
+        <item quantity="other">Справді видалити %d запита на приєднання до групи? Зауважте, що запити не буде відкликано і вас можуть усе одно прийняти або запросити до групи, якщо на запити ще не відповіли.</item>
     </plurals>
     <plurals name="really_delete_incoming_request">
         <item quantity="few">Справді видалити %d запити на приєднання до групи?

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

@@ -558,7 +558,7 @@ http://www.7-zip.org 或 https://itunes.apple.com/us/app/the-unarchiver/id425424
     <string name="prefs_sum_passphrase">需要密码才能解锁本地加密</string>
     <string name="prefs_title_masterkey_change_passphrase">更改密码</string>
     <string name="storage_total">内部存储空间</string>
-    <string name="storage_threema">Threema 占用的空间</string>
+    <string name="storage_threema">%s 占用的空间</string>
     <string name="storage_total_free">总可用空间</string>
     <string name="storage_total_in_use">正在使用</string>
     <string name="one_year">1年</string>
@@ -1399,7 +1399,7 @@ Threema ID。您将不会出现在朋友的联系人列表中。您确定要
     <string name="forward_security_cleared">与此联系人的完全正向保密会话已被清除</string>
     <string name="message_without_forward_security">您收到一条没有完全正向保密的消息,您可以验证发件人是否主动禁用完美前向保密。</string>
     <string name="forward_security_established">此会话中的消息现正受完全正向保密的保护。</string>
-    <string name="forward_security_established_rx">在此会话中收到的消息现正受完全正向保密的保护,您可以在联系人详细信息中启用完全正向保密来保护以后发送的消息。</string>
+    <string name="forward_security_established_rx">在此会话中收到的消息现正受完全正向保密的保护。</string>
     <string name="forward_security_reset">这个之前已启用 “完全正向保密” 的会话,现被重置保密状态。如果对方更换了设备或重新安装此应用程序,就会重置保密状态。</string>
     <string name="forward_security_reset_simple">与此联系人的完全正向保密会话已重置。</string>
     <string name="forward_security_message_out_of_order">解密新收到的消息发生错误,无法解密。</string>

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

@@ -558,7 +558,7 @@ http://www.7-zip.org 或 https://itunes.apple.com/us/app/the-unarchiver/id425424
     <string name="prefs_sum_passphrase">需要密碼才能解鎖本地加密</string>
     <string name="prefs_title_masterkey_change_passphrase">變更密碼</string>
     <string name="storage_total">內部儲存空間</string>
-    <string name="storage_threema">Threema 佔用的空間</string>
+    <string name="storage_threema">%s 佔用的空間</string>
     <string name="storage_total_free">總可用空間</string>
     <string name="storage_total_in_use">正在使用</string>
     <string name="one_year">1年</string>
@@ -1393,7 +1393,7 @@ Threema 支援的所有表情符號。</string>
     <string name="forward_security_cleared">與此聯絡人的完全正向保密對話已被清除</string>
     <string name="message_without_forward_security">您收到一則沒有完全正向保密的訊息,您可以驗證寄件者是否主動停用完美前向保密。</string>
     <string name="forward_security_established">此對話中的訊息現正受完全正向保密的保護。</string>
-    <string name="forward_security_established_rx">在此對話中收到的訊息現正受完全正向保密的保護,您可以在聯絡人詳細資訊中啟用完全正向保密來保護以後傳送的訊息。</string>
+    <string name="forward_security_established_rx">在此對話中收到的訊息現正受完全正向保密的保護。</string>
     <string name="forward_security_reset">這個之前已啟用「完全正向保密」的對話,現被重設保密狀態。如果聯絡人更換了裝置或重新安裝此應用程式,就會重設保密狀態。</string>
     <string name="forward_security_reset_simple">與此聯絡人的完全正向保密對話已重設。</string>
     <string name="forward_security_message_out_of_order">解密新收到的訊息發生錯誤,無法解密。</string>

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

@@ -319,4 +319,5 @@
 	<dimen name="notice_views_elevation">3dp</dimen>
 	<dimen name="notice_views_vertical_margin">2dp</dimen>
     <dimen name="media_gallery_container_radius">3dp</dimen>
+    <dimen name="compose_edittext_elevation">3dp</dimen>
 </resources>

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

@@ -563,7 +563,7 @@
 	<string name="prefs_sum_passphrase">Require a passphrase to unlock local encryption</string>
 	<string name="prefs_title_masterkey_change_passphrase">Change passphrase</string>
 	<string name="storage_total">Space on internal storage</string>
-	<string name="storage_threema">Used by Threema</string>
+	<string name="storage_threema">Used by %s</string>
 	<string name="storage_total_free">Total free space</string>
 	<string name="storage_total_in_use">In use</string>
 	<string name="one_year">1 year</string>
@@ -1414,7 +1414,7 @@
 	<string name="forward_security_cleared">Perfect Forward Secrecy sessions with this contact have been cleared</string>
 	<string name="message_without_forward_security">A message without Perfect Forward Secrecy has been received. Verify that the sender has intentionally disabled Perfect Forward Secrecy.</string>
 	<string name="forward_security_established">Messages in this chat are now protected by Perfect Forward Secrecy.</string>
-	<string name="forward_security_established_rx">Messages received in this chat are now protected by Perfect Forward Secrecy. Enable Perfect Forward Secrecy in contact details to protect sent messages as well.</string>
+	<string name="forward_security_established_rx">Messages received in this chat are now protected by Perfect Forward Secrecy.</string>
 	<string name="forward_security_reset">The Perfect Forward Secrecy session was reset. This can happen when the chat partner changes devices or reinstalls the app.</string>
 	<string name="forward_security_reset_simple">The Perfect Forward Secrecy session with this contact has been reset.</string>
 	<string name="forward_security_message_out_of_order">A message was received out of order and could not be decrypted.</string>

+ 6 - 3
app/src/main/res/values/styles.xml

@@ -894,7 +894,9 @@
 		<item name="cardBackgroundColor">?attr/colorOnSurfaceInverse</item>
 	</style>
 
-	<style name="Threema.CardView.TopRounded" parent="@style/Widget.Material3.CardView.Filled">
+	<style name="Threema.CardView.MentionSelector" parent="@style/Widget.Material3.CardView.Filled">
+		<item name="cardBackgroundColor">?attr/colorSurface</item>
+		<item name="cardElevation">@dimen/compose_edittext_elevation</item>
 		<item name="shapeAppearanceOverlay">@style/Threema.ShapeAppearance.CardView.TopRounded</item>
 	</style>
 
@@ -960,8 +962,8 @@
 
 	<style name="Threema.ShapeAppearance.CardView.TopRounded" parent="">
 		<item name="cornerFamily">rounded</item>
-		<item name="cornerSizeTopRight">@dimen/cardview_border_radius</item>
-		<item name="cornerSizeTopLeft">@dimen/cardview_border_radius</item>
+		<item name="cornerSizeTopRight">24dp</item>
+		<item name="cornerSizeTopLeft">24dp</item>
 		<item name="cornerSizeBottomRight">0dp</item>
 		<item name="cornerSizeBottomLeft">0dp</item>
 	</style>
@@ -1065,6 +1067,7 @@
 		<item name="boxCornerRadiusBottomEnd">24dp</item>
 		<item name="boxCornerRadiusTopStart">24dp</item>
 		<item name="boxCornerRadiusTopEnd">24dp</item>
+		<item name="boxBackgroundColor">?attr/colorSecondaryContainer</item>
 	</style>
 
 	<style name="Threema.EditText.Compose" parent="@style/Widget.Material3.TextInputEditText.FilledBox">

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

@@ -31,8 +31,8 @@
 	<string name="private_threema_download"><![CDATA[Wenn Sie die App privat nutzen möchten, laden Sie Threema bitte <a href="https://play.google.com/store/apps/details?id=ch.threema.app">hier</a> herunter.]]></string>
 	<string name="safe_configure_server_explain">Speichern Sie Ihr Threema Safe-Backup auf dem Server Ihrer Organisation, oder legen Sie einen anderen Backup-Server fest.</string>
 	<string name="directory_search">Im Verzeichnis suchen</string>
-	<string name="directory_title">Firmenverzeichnis</string>
-	<string name="directory_empty_view_text">Bitte geben Sie mindestens 3 Zeichen eines Namens ein, um mit der Suche im Firmenverzeichnis zu beginnen oder wählen Sie eine Kategorie, indem Sie auf das Filter-Symbol tippen.</string>
+	<string name="directory_title">Unternehmensverzeichnis</string>
+	<string name="directory_empty_view_text">Bitte geben Sie mindestens 3 Zeichen eines Namens ein, um mit der Suche im Unternehmensverzeichnis zu beginnen oder wählen Sie eine Kategorie, indem Sie auf das Filter-Symbol tippen.</string>
 	<string name="share_with_app">Mit Threema OnPrem teilen</string>
 </resources>
 

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

@@ -27,7 +27,7 @@
 	<string name="menu_about">Über Threema Red</string>
 	<string name="private_threema_download"><![CDATA[Wenn Sie die App privat nutzen möchten, laden Sie Threema bitte <a href=%s>hier</a> herunter.]]></string>
 	<string name="directory_search">Im Verzeichnis suchen</string>
-	<string name="directory_title">Firmenverzeichnis</string>
-	<string name="directory_empty_view_text">Bitte geben Sie mindestens 3 Zeichen eines Namens ein, um mit der Suche im Firmenverzeichnis zu beginnen oder wählen Sie eine Kategorie, indem Sie auf das Filter-Symbol tippen.</string>
+	<string name="directory_title">Unternehmensverzeichnis</string>
+	<string name="directory_empty_view_text">Bitte geben Sie mindestens 3 Zeichen eines Namens ein, um mit der Suche im Unternehmensverzeichnis zu beginnen oder wählen Sie eine Kategorie, indem Sie auf das Filter-Symbol tippen.</string>
 </resources>
 

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

@@ -27,8 +27,8 @@
 	<string name="menu_about">Über Threema Work</string>
 	<string name="private_threema_download"><![CDATA[ <a href=%1$s>Erfahren Sie mehr über Threema Work</a> <br/><br/> Wenn Sie die App privat nutzen möchten, laden Sie Threema bitte <a href=%2$s>hier</a> herunter.]]></string>
 	<string name="directory_search">Im Verzeichnis suchen</string>
-	<string name="directory_title">Firmenverzeichnis</string>
-	<string name="directory_empty_view_text">Bitte geben Sie mindestens 3 Zeichen eines Namens ein, um mit der Suche im Firmenverzeichnis zu beginnen oder wählen Sie eine Kategorie, indem Sie auf das Filter-Symbol tippen.</string>
+	<string name="directory_title">Unternehmensverzeichnis</string>
+	<string name="directory_empty_view_text">Bitte geben Sie mindestens 3 Zeichen eines Namens ein, um mit der Suche im Unternehmensverzeichnis zu beginnen oder wählen Sie eine Kategorie, indem Sie auf das Filter-Symbol tippen.</string>
 	<string name="share_with_app">Mit Threema Work teilen</string>
 </resources>
 

+ 13 - 6
domain/src/main/java/ch/threema/domain/protocol/csp/connection/MessageProcessorInterface.java

@@ -25,6 +25,7 @@ import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 import ch.threema.domain.protocol.csp.coders.MessageBox;
+import ch.threema.domain.protocol.csp.fs.ForwardSecurityMessageProcessor.PeerRatchetIdentifier;
 
 /**
  * Interface for objects that wish to process incoming messages from the server.
@@ -33,10 +34,12 @@ public interface MessageProcessorInterface {
 	class ProcessIncomingResult {
 		private final boolean processed;
 		private final @Nullable Integer type;
+		private final @Nullable PeerRatchetIdentifier peerRatchet;
 
-		private ProcessIncomingResult(boolean processed, @Nullable Integer type) {
+		private ProcessIncomingResult(boolean processed, @Nullable Integer type, @Nullable PeerRatchetIdentifier peerRatchet) {
 			this.processed = processed;
 			this.type = type;
+			this.peerRatchet = peerRatchet;
 		}
 
 		/**
@@ -45,15 +48,15 @@ public interface MessageProcessorInterface {
 		 */
 		@NonNull
 		public static ProcessIncomingResult failed() {
-			return new ProcessIncomingResult(false, null);
+			return new ProcessIncomingResult(false, null, null);
 		}
 
 		/**
 		 * A message was successfully processed. It should be acked towards the chat server.
 		 */
 		@NonNull
-		public static ProcessIncomingResult processed() {
-			return new ProcessIncomingResult(true, null);
+		public static ProcessIncomingResult processed(@Nullable PeerRatchetIdentifier peerRatchet) {
+			return new ProcessIncomingResult(true, null, peerRatchet);
 		}
 
 		/**
@@ -62,8 +65,8 @@ public interface MessageProcessorInterface {
 		 * @param type The type of the message if known.
 		 */
 		@NonNull
-		public static ProcessIncomingResult processed(@Nullable Integer type) {
-			return new ProcessIncomingResult(true, type);
+		public static ProcessIncomingResult processed(@Nullable Integer type, @Nullable PeerRatchetIdentifier peerRatchet) {
+			return new ProcessIncomingResult(true, type, peerRatchet);
 		}
 
 		public boolean wasProcessed() {
@@ -73,6 +76,10 @@ public interface MessageProcessorInterface {
 		public boolean hasType(int type) {
 			return this.type != null && this.type == type;
 		}
+
+		public @Nullable PeerRatchetIdentifier getPeerRatchetIdentifier() {
+			return this.peerRatchet;
+		}
 	}
 
 	/**

+ 28 - 16
domain/src/main/java/ch/threema/domain/protocol/csp/connection/ThreemaConnection.java

@@ -54,6 +54,7 @@ import java.util.concurrent.ExecutionException;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 import ch.threema.base.ThreemaException;
 import ch.threema.base.crypto.NonceCounter;
@@ -68,6 +69,7 @@ import ch.threema.domain.protocol.ServerAddressProvider;
 import ch.threema.domain.protocol.Version;
 import ch.threema.domain.protocol.csp.ProtocolDefines;
 import ch.threema.domain.protocol.csp.coders.MessageBox;
+import ch.threema.domain.protocol.csp.fs.ForwardSecurityMessageProcessor;
 import ch.threema.domain.stores.IdentityStoreInterface;
 import ove.crypto.digest.Blake2b;
 
@@ -88,8 +90,9 @@ public class ThreemaConnection implements Runnable {
 
 	/* Delegate objects */
 	private final IdentityStoreInterface identityStore;
-	private final NonceFactory nonceFactory;
+	private final @NonNull NonceFactory nonceFactory;
 	private MessageProcessorInterface messageProcessor;
+	private @Nullable ForwardSecurityMessageProcessor forwardSecurityMessageProcessor;
 	private DeviceCookieManager deviceCookieManager;
 
 	/* Server address object */
@@ -134,7 +137,7 @@ public class ThreemaConnection implements Runnable {
 	 * @param ipv6 whether to use IPv4+IPv6 for connection, or only IPv4
 	 */
 	public ThreemaConnection(IdentityStoreInterface identityStore,
-							 NonceFactory nonceFactory,
+							 @NonNull NonceFactory nonceFactory,
 	                         ServerAddressProvider serverAddressProvider,
 	                         boolean ipv6) {
 		this.identityStore = identityStore;
@@ -149,7 +152,7 @@ public class ThreemaConnection implements Runnable {
 		timer = new Timer(/*"ThreemaConnectionTimer", */true);
 
 		ackListeners = new HashSet<>();
-		connectionStateListeners = new HashSet<>();
+		connectionStateListeners = new CopyOnWriteArraySet<>();
 		queueSendCompleteListeners = new CopyOnWriteArraySet<>();
 
 		lastAlertMessages = new HashSet<>();
@@ -167,6 +170,14 @@ public class ThreemaConnection implements Runnable {
 		this.messageProcessor = messageProcessor;
 	}
 
+	public @Nullable ForwardSecurityMessageProcessor getForwardSecurityMessageProcessor() {
+		return forwardSecurityMessageProcessor;
+	}
+
+	public void setForwardSecurityMessageProcessor(@NonNull ForwardSecurityMessageProcessor forwardSecurityMessageProcessor) {
+		this.forwardSecurityMessageProcessor = forwardSecurityMessageProcessor;
+	}
+
 	public void setServerAddressProvider(ServerAddressProvider serverAddressProvider) {
 		this.serverAddressProvider = serverAddressProvider;
 	}
@@ -612,28 +623,22 @@ public class ThreemaConnection implements Runnable {
 	}
 
 	public void addConnectionStateListener(ConnectionStateListener listener) {
-		synchronized (connectionStateListeners) {
-			connectionStateListeners.add(listener);
-		}
+		connectionStateListeners.add(listener);
 	}
 
 	public void removeConnectionStateListener(ConnectionStateListener listener) {
-		synchronized (connectionStateListeners) {
-			connectionStateListeners.remove(listener);
-		}
+		connectionStateListeners.remove(listener);
 	}
 
 	private void setConnectionState(ConnectionState state) {
 		this.state = state;
 
 		if (curSocketAddressIndex < serverSocketAddresses.size()) {
-			synchronized (connectionStateListeners) {
-				for (ConnectionStateListener listener : connectionStateListeners) {
-					try {
-						listener.updateConnectionState(state, serverSocketAddresses.get(curSocketAddressIndex));
-					} catch (Exception e) {
-						logger.warn("Exception while invoking connection state listener", e);
-					}
+			for (ConnectionStateListener listener : connectionStateListeners) {
+				try {
+					listener.updateConnectionState(state, serverSocketAddresses.get(curSocketAddressIndex));
+				} catch (Exception e) {
+					logger.warn("Exception while invoking connection state listener", e);
 				}
 			}
 		}
@@ -784,8 +789,15 @@ public class ThreemaConnection implements Runnable {
 						this.nonceFactory.store(boxmsg.getNonce());
 					}
 
+					// As soon as the result has been processed and the nonce has been saved, we
+					// turn the peer ratchet and store it.
+					if (forwardSecurityMessageProcessor != null && result.wasProcessed() && result.getPeerRatchetIdentifier() != null) {
+						forwardSecurityMessageProcessor.commitPeerRatchet(result.getPeerRatchetIdentifier());
+					}
+
 					ackMessage = result.wasProcessed();
 				} else {
+					logger.info("Skipped processing message {} as its nonce has already been used", boxmsg.getMessageId());
 					// auto ack a already nonce'd message
 					ackMessage = true;
 				}

+ 115 - 18
domain/src/main/java/ch/threema/domain/protocol/csp/fs/ForwardSecurityMessageProcessor.java

@@ -68,6 +68,63 @@ public class ForwardSecurityMessageProcessor {
 	private final @NonNull MessageQueue messageQueue;
 	private final @NonNull ForwardSecurityFailureListener failureListener;
 
+	/**
+	 * When decrypting a message, we get this decryption result. It contains the unencrypted message
+	 * as well as information about the forward security session. This information is used to
+	 * finalize the peer ratchet state after the message has been completely processed.
+	 */
+	public static class ForwardSecurityDecryptionResult {
+		/**
+		 * The unencrypted message. Null if it is a forward security control message.
+		 */
+		public final @Nullable AbstractMessage message;
+
+		/**
+		 * The information to identify the ratchet that was used to decrypt the message.
+		 */
+		public final @Nullable PeerRatchetIdentifier peerRatchetIdentifier;
+
+		public static final ForwardSecurityDecryptionResult NONE = new ForwardSecurityDecryptionResult(null, null);
+
+		public ForwardSecurityDecryptionResult(
+			@Nullable AbstractMessage message,
+			@Nullable PeerRatchetIdentifier peerRatchetIdentifier
+		) {
+			this.message = message;
+			this.peerRatchetIdentifier = peerRatchetIdentifier;
+		}
+	}
+
+	/**
+	 * Contains the information about the session and ratchet that was used to decrypt a message.
+	 */
+	public static class PeerRatchetIdentifier {
+		/**
+		 * The session id of the dh session.
+		 */
+		public final @NonNull DHSessionId sessionId;
+
+		/**
+		 * The peer identity of the session.
+		 */
+		public final @NonNull String peerIdentity;
+
+		/**
+		 * The dh type of the received message.
+		 */
+		public final @NonNull Encapsulated.DHType dhType;
+
+		public PeerRatchetIdentifier(
+			@NonNull DHSessionId sessionId,
+			@NonNull String peerIdentity,
+			@NonNull Encapsulated.DHType dhType
+		) {
+			this.sessionId = sessionId;
+			this.peerIdentity = peerIdentity;
+			this.dhType = dhType;
+		}
+	}
+
 	private interface ForwardSecurityStatusWrapper extends ForwardSecurityStatusListener {
 		void setStatusListener(ForwardSecurityStatusListener forwardSecurityStatusListener);
 	}
@@ -222,7 +279,9 @@ public class ForwardSecurityMessageProcessor {
 				logger.error("Unable to delete DH session", e);
 			}
 			// Show a status message to the user
-			statusListener.postIllegalSessionState(sessionId, contact);
+			if (contact != null) {
+				statusListener.postIllegalSessionState(sessionId, contact);
+			}
 		});
 	}
 
@@ -234,7 +293,7 @@ public class ForwardSecurityMessageProcessor {
 	 *
 	 * @return Decapsulated message, if any, or null in case of a control message that has been consumed and does not need further processing
 	 */
-	public synchronized @Nullable AbstractMessage processEnvelopeMessage(
+	public synchronized @NonNull ForwardSecurityDecryptionResult processEnvelopeMessage(
 		@NonNull Contact sender,
 		@NonNull ForwardSecurityEnvelopeMessage envelopeMessage
 	) throws ThreemaException, BadMessageException {
@@ -255,7 +314,7 @@ public class ForwardSecurityMessageProcessor {
 			throw new UnknownMessageTypeException("Unsupported message type");
 		}
 
-		return null;
+		return ForwardSecurityDecryptionResult.NONE;
 	}
 
 	public synchronized @NonNull ForwardSecurityEnvelopeMessage makeMessage(
@@ -394,12 +453,47 @@ public class ForwardSecurityMessageProcessor {
 		}
 	}
 
+	/**
+	 * Turn and commit the peer ratchet. Call this method after an incoming message has been
+	 * processed completely.
+	 *
+	 * @param peerRatchetIdentifier the information needed to identify the corresponding ratchet
+	 */
+	public synchronized void commitPeerRatchet(@NonNull PeerRatchetIdentifier peerRatchetIdentifier) throws DHSessionStoreException {
+		DHSessionId sessionId = peerRatchetIdentifier.sessionId;
+		String peerIdentity = peerRatchetIdentifier.peerIdentity;
+		Encapsulated.DHType dhType = peerRatchetIdentifier.dhType;
+
+		DHSession session = dhSessionStoreInterface.getDHSession(identityStoreInterface.getIdentity(), peerIdentity, sessionId);
+		if (session == null) {
+			logger.warn("Could not find session {}. Ratchet of type {} can not be turned for the last received message from {}", sessionId, dhType, peerIdentity);
+			return;
+		}
+
+		KDFRatchet ratchet = null;
+		switch (dhType) {
+			case TWODH:
+				ratchet = session.getPeerRatchet2DH();
+				break;
+			case FOURDH:
+				ratchet = session.getPeerRatchet4DH();
+				break;
+		}
+		if (ratchet == null) {
+			logger.warn("Ratchet of type {} is null in session {} with contact {}", dhType, sessionId, peerIdentity);
+			return;
+		}
+
+		ratchet.turn();
+		dhSessionStoreInterface.storeDHSession(session);
+	}
+
 	/**
 	 * Clear all sessions with the peer contact and send a terminate message for each of those.
 	 *
 	 * @param contact the peer contact
 	 */
-	public void clearAndTerminateAllSessions(@NonNull Contact contact, @NonNull Terminate.Cause cause) {
+	public synchronized void clearAndTerminateAllSessions(@NonNull Contact contact, @NonNull Terminate.Cause cause) {
 		try {
 			String myIdentity = identityStoreInterface.getIdentity();
 			String peerIdentity = contact.getIdentity();
@@ -507,19 +601,19 @@ public class ForwardSecurityMessageProcessor {
 		statusListener.rejectReceived(reject, contact, session, statusListener.hasForwardSecuritySupport(contact));
 	}
 
-	private @Nullable AbstractMessage processMessage(@NonNull Contact contact, @NonNull ForwardSecurityEnvelopeMessage envelopeMessage)
+	private @NonNull ForwardSecurityDecryptionResult processMessage(@NonNull Contact contact, @NonNull ForwardSecurityEnvelopeMessage envelopeMessage)
 		throws ThreemaException, BadMessageException {
 
-		ForwardSecurityDataMessage message = (ForwardSecurityDataMessage)envelopeMessage.getData();
+		final ForwardSecurityDataMessage message = (ForwardSecurityDataMessage)envelopeMessage.getData();
 
-		DHSession session = dhSessionStoreInterface.getDHSession(identityStoreInterface.getIdentity(), contact.getIdentity(), message.getSessionId());
+		final DHSession session = dhSessionStoreInterface.getDHSession(identityStoreInterface.getIdentity(), contact.getIdentity(), message.getSessionId());
 		if (session == null) {
 			// Session not found, probably lost local data or old message
 			logger.warn("No DH session found for message {} in session ID {} from {}", envelopeMessage.getMessageId(), message.getSessionId(), contact.getIdentity());
 			ForwardSecurityDataReject reject = new ForwardSecurityDataReject(message.getSessionId(), envelopeMessage.getMessageId(), Reject.Cause.UNKNOWN_SESSION);
 			sendMessageToContact(contact, reject);
 			statusListener.sessionForMessageNotFound(message.getSessionId(), envelopeMessage.getMessageId(), contact);
-			return null;
+			return ForwardSecurityDecryptionResult.NONE;
 		}
 
 		// Validate offered and applied version
@@ -534,7 +628,7 @@ public class ForwardSecurityMessageProcessor {
 			dhSessionStoreInterface.deleteDHSession(identityStoreInterface.getIdentity(), contact.getIdentity(), session.getId());
 			// TODO(SE-354): Should we supply an error cause for the UI here? Otherwise this looks as if the remote willingly terminated.
 			statusListener.sessionTerminated(message.getSessionId(), contact, false, true);
-			return null;
+			return ForwardSecurityDecryptionResult.NONE;
 		}
 
 		// Obtain appropriate ratchet and turn to match the message's counter value
@@ -561,7 +655,7 @@ public class ForwardSecurityMessageProcessor {
 			dhSessionStoreInterface.deleteDHSession(identityStoreInterface.getIdentity(), contact.getIdentity(), session.getId());
 			// TODO(SE-354): Should we supply an error cause for the UI here? Otherwise this looks as if the remote willingly terminated.
 			statusListener.sessionTerminated(message.getSessionId(), contact, false, true);
-			return null;
+			return ForwardSecurityDecryptionResult.NONE;
 		}
 
 		// We should already be at the correct ratchet count since we increment it after
@@ -588,7 +682,7 @@ public class ForwardSecurityMessageProcessor {
 			dhSessionStoreInterface.deleteDHSession(identityStoreInterface.getIdentity(), contact.getIdentity(), session.getId());
 			// TODO(SE-354): Should we supply an error cause for the UI here? Otherwise this looks as if the remote willingly terminated.
 			statusListener.sessionTerminated(message.getSessionId(), contact, false, true);
-			return null;
+			return ForwardSecurityDecryptionResult.NONE;
 		}
 
 		logger.debug("Decapsulated message from {} (message-id={}, mode={}, session={}, offered-version={}, applied-version={})",
@@ -606,10 +700,6 @@ public class ForwardSecurityMessageProcessor {
 			statusListener.versionsUpdated(session, updatedVersionsSnapshot, contact);
 		}
 
-		// Turn the ratchet once, as we will not need the current encryption key anymore and the
-		// next message from the peer must have a ratchet count of at least one higher
-		ratchet.turn();
-
 		if (mode == ForwardSecurityMode.FOURDH) {
 			// If this was a 4DH message, then we should erase the 2DH peer ratchet, as we shall not
 			// receive (or send) any further 2DH messages in this session. Note that this is also
@@ -632,14 +722,21 @@ public class ForwardSecurityMessageProcessor {
 			}
 		}
 
-		// Save session, as ratchets and negotiated version may have changed
+		// Save session, as ratchets and negotiated version may have changed. Note that the peer
+		// ratchet is not yet turned at this point. This is required for being able to reprocess the
+		// last message when processing it is aborted.
 		dhSessionStoreInterface.storeDHSession(session);
 
-		// Decode inner message and pass it to processor
+		// Decode inner message
 		AbstractMessage innerMsg = new MessageCoder(contactStore, identityStoreInterface)
 			.decodeEncapsulated(plaintext, envelopeMessage, processedVersions.appliedVersion, contact);
 		innerMsg.setForwardSecurityMode(mode);
-		return innerMsg;
+
+		// Collect the information needed to identify the used ratchet
+		PeerRatchetIdentifier ratchetIdentifier = new PeerRatchetIdentifier(session.getId(), contact.getIdentity(), message.getType());
+
+		// Pass the inner message and the ratchet information to the message processor
+		return new ForwardSecurityDecryptionResult(innerMsg, ratchetIdentifier);
 	}
 
 	private void processTerminate(@NonNull Contact contact, @NonNull ForwardSecurityDataTerminate message) throws DHSessionStoreException {

+ 69 - 11
domain/src/test/java/ch/threema/domain/protocol/csp/fs/ForwardSecurityMessageProcessorTest.java

@@ -31,9 +31,11 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Objects;
 
+import androidx.annotation.NonNull;
 import ch.threema.base.ThreemaException;
 import ch.threema.domain.fs.DHSession;
 import ch.threema.domain.fs.DHSessionId;
+import ch.threema.domain.fs.KDFRatchet;
 import ch.threema.domain.helpers.DummyUsers;
 import ch.threema.domain.protocol.csp.coders.MessageBox;
 import ch.threema.domain.protocol.csp.coders.MessageCoder;
@@ -224,7 +226,9 @@ public class ForwardSecurityMessageProcessorTest {
 
 		// Let Bob process all the messages that he has received from Alice.
 		// The decapsulated message should be the text message from Alice.
-		receiveAndAssertSingleMessage(aliceContext.messageQueue, bobContext, ALICE_MESSAGE_4, ForwardSecurityMode.FOURDH);
+		AbstractMessage msg = processOneReceivedMessage(aliceContext.messageQueue, bobContext, 1, false);
+		Assert.assertEquals(ALICE_MESSAGE_4, ((BoxTextMessage)msg).getText());
+		Assert.assertEquals(ForwardSecurityMode.FOURDH, msg.getForwardSecurityMode());
 
 		// At this point, Bob should not have enqueued any further messages
 		Assert.assertEquals(0, bobContext.messageQueue.getQueueSize());
@@ -394,7 +398,7 @@ public class ForwardSecurityMessageProcessorTest {
 		Assert.assertEquals(Version.V1_0.getNumber(), DHSession.SUPPORTED_VERSION_RANGE.getMax());
 		// Note that Bob only processes one message, i.e. the init message. He does not yet process
 		// the text message
-		processOneReceivedMessage(aliceContext.messageQueue, bobContext);
+		processOneReceivedMessage(aliceContext.messageQueue, bobContext, 0, false);
 
 		// Alice should process the accept message now (while supporting version 1.1)
 		setSupportedVersionRange(
@@ -557,12 +561,12 @@ public class ForwardSecurityMessageProcessorTest {
 
 		// Now Bob processes the text message from Alice. Note that the message should be rejected
 		// and therefore return an empty list.
-		Assert.assertEquals(0, processReceivedMessages(aliceContext.messageQueue, bobContext).size());
+		Assert.assertNull(processOneReceivedMessage(aliceContext.messageQueue, bobContext, 0, true));
 		Assert.assertNull(bobContext.dhSessionStore.getBestDHSession(bobContext.identityStore.getIdentity(), aliceContext.identityStore.getIdentity()));
 
 		// Assert that Alice did receive a session reject
 		Assert.assertEquals(1, bobContext.messageQueue.getQueueSize());
-		Assert.assertNull(processOneReceivedMessage(bobContext.messageQueue, aliceContext));
+		Assert.assertNull(processOneReceivedMessage(bobContext.messageQueue, aliceContext, 0, true));
 		Assert.assertNull(aliceContext.dhSessionStore.getBestDHSession(
 			DummyUsers.ALICE.getIdentity(), DummyUsers.BOB.getIdentity()
 		));
@@ -580,7 +584,7 @@ public class ForwardSecurityMessageProcessorTest {
 		Assert.assertEquals(DHSession.State.L20, aliceInitialSession.getState());
 
 		// Bob processes the init and should now have a session in state R24
-		processOneReceivedMessage(aliceContext.messageQueue, bobContext);
+		processOneReceivedMessage(aliceContext.messageQueue, bobContext, 0, false);
 
 		DHSession bobInitialSession = bobContext.dhSessionStore.getBestDHSession(
 			DummyUsers.BOB.getIdentity(), DummyUsers.ALICE.getIdentity()
@@ -592,7 +596,7 @@ public class ForwardSecurityMessageProcessorTest {
 		receiveAndAssertSingleMessage(aliceContext.messageQueue, bobContext, ALICE_MESSAGE_1, ForwardSecurityMode.TWODH);
 
 		// Alice should now process the accept from Bob and update the state to L44
-		processOneReceivedMessage(bobContext.messageQueue, aliceContext);
+		processOneReceivedMessage(bobContext.messageQueue, aliceContext, 0, false);
 
 		DHSession aliceFinalSession = aliceContext.dhSessionStore.getBestDHSession(
 			DummyUsers.ALICE.getIdentity(), DummyUsers.BOB.getIdentity()
@@ -796,7 +800,7 @@ public class ForwardSecurityMessageProcessorTest {
 	private List<AbstractMessage> processReceivedMessages(MessageQueue sourceQueue, UserContext recipientContext) throws BadMessageException, ThreemaException, MissingPublicKeyException {
 		List<AbstractMessage> decapsulatedMessages = new LinkedList<>();
 		while (sourceQueue.getQueueSize() > 0) {
-			AbstractMessage decapMsg = processOneReceivedMessage(sourceQueue, recipientContext);
+			AbstractMessage decapMsg = processOneReceivedMessage(sourceQueue, recipientContext, 0, false);
 			if (decapMsg != null) {
 				decapsulatedMessages.add(decapMsg);
 			}
@@ -804,17 +808,71 @@ public class ForwardSecurityMessageProcessorTest {
 		return decapsulatedMessages;
 	}
 
-	private AbstractMessage processOneReceivedMessage(MessageQueue sourceQueue, UserContext recipientContext) throws BadMessageException, ThreemaException, MissingPublicKeyException {
+	private AbstractMessage processOneReceivedMessage(
+		MessageQueue sourceQueue,
+		UserContext recipientContext,
+		long numSkippedMessages,
+		boolean shouldSessionBeDeleted
+	) throws BadMessageException, ThreemaException, MissingPublicKeyException {
 		MessageBox messageBox = sourceQueue.getQueue().remove(0);
 		Assert.assertNotNull(messageBox);
 
 		MessageCoder messageCoder = new MessageCoder(recipientContext.contactStore, recipientContext.identityStore);
-		AbstractMessage msg = messageCoder.decode(messageBox, false);
+		ForwardSecurityEnvelopeMessage msg = (ForwardSecurityEnvelopeMessage) messageCoder.decode(messageBox, false);
 
-		return recipientContext.fsmp.processEnvelopeMessage(
+		long counterBeforeProcessing = getRatchetCounterInSession(recipientContext, msg);
+
+		ForwardSecurityMessageProcessor.ForwardSecurityDecryptionResult result = recipientContext.fsmp.processEnvelopeMessage(
 			Objects.requireNonNull(recipientContext.contactStore.getContactForIdentity(msg.getFromIdentity())),
-			(ForwardSecurityEnvelopeMessage) msg
+			msg
 		);
+
+		long counterAfterProcessing = getRatchetCounterInSession(recipientContext, msg);
+
+		if (result.peerRatchetIdentifier != null) {
+			recipientContext.fsmp.commitPeerRatchet(result.peerRatchetIdentifier);
+		}
+
+		long counterAfterCommittingRatchet = getRatchetCounterInSession(recipientContext, msg);
+
+		if (!shouldSessionBeDeleted) {
+			Assert.assertEquals("Ratchet counter should be exactly increased by the number of skipped messages:", counterBeforeProcessing + numSkippedMessages, counterAfterProcessing);
+		}
+
+		if (result.peerRatchetIdentifier != null) {
+			if (shouldSessionBeDeleted) {
+				Assert.assertEquals("Session should be deleted:", -1, counterAfterCommittingRatchet);
+			} else {
+				Assert.assertEquals("Ratchet counter should be increased when turning the ratchet:", counterAfterProcessing + 1, counterAfterCommittingRatchet);
+			}
+		}
+
+		return result.message;
+	}
+
+	private long getRatchetCounterInSession(@NonNull UserContext ctx, @NonNull ForwardSecurityEnvelopeMessage msg) throws DHSessionStoreException {
+		if (!(msg.getData() instanceof ForwardSecurityDataMessage)) {
+			return 0;
+		}
+
+		DHSession session = ctx.dhSessionStore.getDHSession(ctx.identityStore.getIdentity(), msg.getFromIdentity(), msg.getData().getSessionId());
+		if (session == null) {
+			return -1;
+		}
+		KDFRatchet ratchet = null;
+		switch (((ForwardSecurityDataMessage)msg.getData()).getType()) {
+			case TWODH:
+				ratchet = session.getPeerRatchet2DH();
+				break;
+			case FOURDH:
+				ratchet = session.getPeerRatchet4DH();
+				break;
+		}
+		if (ratchet == null) {
+			return -1;
+		} else {
+			return ratchet.getCounter();
+		}
 	}
 
 	private AbstractMessage sendTextMessage(String message, UserContext senderContext, DummyUsers.User recipient) throws ThreemaException, ForwardSecurityMessageProcessor.MessageTypeNotSupportedInSession {