Browse Source

Version 4.57

Threema 4 năm trước cách đây
mục cha
commit
81e456bc91
100 tập tin đã thay đổi với 1576 bổ sung880 xóa
  1. 13 14
      app/build.gradle
  2. 1 1
      app/src/google_services_based/java/ch/threema/app/wearable/WearableHandler.java
  3. 2 2
      app/src/main/java/ch/threema/app/activities/ContactDetailActivity.java
  4. 63 71
      app/src/main/java/ch/threema/app/activities/GroupDetailActivity.java
  5. 13 11
      app/src/main/java/ch/threema/app/activities/HomeActivity.java
  6. 6 1
      app/src/main/java/ch/threema/app/activities/MemberChooseActivity.java
  7. 9 10
      app/src/main/java/ch/threema/app/activities/RecipientListBaseActivity.java
  8. 2 2
      app/src/main/java/ch/threema/app/activities/SendMediaActivity.java
  9. 10 29
      app/src/main/java/ch/threema/app/activities/StorageManagementActivity.java
  10. 5 5
      app/src/main/java/ch/threema/app/activities/ThreemaToolbarActivity.java
  11. 2 2
      app/src/main/java/ch/threema/app/activities/wizard/WizardBaseActivity.java
  12. 34 2
      app/src/main/java/ch/threema/app/adapters/ContactDetailAdapter.java
  13. 18 10
      app/src/main/java/ch/threema/app/adapters/ContactListAdapter.java
  14. 3 1
      app/src/main/java/ch/threema/app/dialogs/MessageDetailDialog.java
  15. 8 5
      app/src/main/java/ch/threema/app/fragments/ComposeMessageFragment.java
  16. 40 25
      app/src/main/java/ch/threema/app/fragments/ContactsSectionFragment.java
  17. 13 26
      app/src/main/java/ch/threema/app/fragments/MyIDFragment.java
  18. 5 0
      app/src/main/java/ch/threema/app/fragments/UserMemberListFragment.java
  19. 5 0
      app/src/main/java/ch/threema/app/fragments/WorkUserListFragment.java
  20. 5 0
      app/src/main/java/ch/threema/app/fragments/WorkUserMemberListFragment.java
  21. 18 0
      app/src/main/java/ch/threema/app/fragments/wizard/WizardFragment1.java
  22. 23 5
      app/src/main/java/ch/threema/app/fragments/wizard/WizardFragment2.java
  23. 31 5
      app/src/main/java/ch/threema/app/fragments/wizard/WizardFragment3.java
  24. 1 2
      app/src/main/java/ch/threema/app/managers/ServiceManager.java
  25. 4 4
      app/src/main/java/ch/threema/app/mediaattacher/MediaAttachActivity.java
  26. 5 1
      app/src/main/java/ch/threema/app/mediaattacher/MediaSelectionBaseActivity.java
  27. 5 2
      app/src/main/java/ch/threema/app/notifications/NotificationBuilderWrapper.java
  28. 56 24
      app/src/main/java/ch/threema/app/preference/SettingsPrivacyFragment.java
  29. 4 0
      app/src/main/java/ch/threema/app/processors/MessageProcessor.java
  30. 90 76
      app/src/main/java/ch/threema/app/routines/SynchronizeContactsRoutine.java
  31. 10 2
      app/src/main/java/ch/threema/app/services/ContactService.java
  32. 87 44
      app/src/main/java/ch/threema/app/services/ContactServiceImpl.java
  33. 11 6
      app/src/main/java/ch/threema/app/services/GroupApiServiceImpl.java
  34. 8 8
      app/src/main/java/ch/threema/app/services/GroupServiceImpl.java
  35. 14 2
      app/src/main/java/ch/threema/app/services/MessageServiceImpl.java
  36. 1 1
      app/src/main/java/ch/threema/app/services/NotificationServiceImpl.java
  37. 2 2
      app/src/main/java/ch/threema/app/services/SynchronizeContactsServiceImpl.java
  38. 25 1
      app/src/main/java/ch/threema/app/services/UserServiceImpl.java
  39. 5 0
      app/src/main/java/ch/threema/app/services/systemupdate/SystemUpdateToVersion39.java
  40. 63 0
      app/src/main/java/ch/threema/app/services/systemupdate/SystemUpdateToVersion66.java
  41. 11 80
      app/src/main/java/ch/threema/app/stores/ContactStore.java
  42. 6 0
      app/src/main/java/ch/threema/app/threemasafe/ThreemaSafeServiceImpl.java
  43. 4 3
      app/src/main/java/ch/threema/app/ui/MentionSelectorPopup.java
  44. 73 48
      app/src/main/java/ch/threema/app/utils/AndroidContactUtil.java
  45. 26 32
      app/src/main/java/ch/threema/app/utils/ContactUtil.java
  46. 8 0
      app/src/main/java/ch/threema/app/utils/MediaPlayerStateWrapper.java
  47. 3 3
      app/src/main/java/ch/threema/app/utils/QuoteUtil.java
  48. 1 0
      app/src/main/java/ch/threema/app/utils/SynchronizeContactsUtil.java
  49. 27 11
      app/src/main/java/ch/threema/app/voicemessage/VoiceRecorderActivity.java
  50. 4 3
      app/src/main/java/ch/threema/app/voip/PeerConnectionClient.java
  51. 16 3
      app/src/main/java/ch/threema/app/voip/activities/CallActivity.java
  52. 4 4
      app/src/main/java/ch/threema/app/voip/services/VoipCallService.java
  53. 56 9
      app/src/main/java/ch/threema/app/voip/services/VoipStateService.java
  54. 5 0
      app/src/main/java/ch/threema/app/webclient/converter/Contact.java
  55. 6 1
      app/src/main/java/ch/threema/app/workers/IdentityStatesWorker.java
  56. 25 4
      app/src/main/java/ch/threema/client/AbstractMessage.java
  57. 1 8
      app/src/main/java/ch/threema/client/ContactStoreInterface.java
  58. 0 3
      app/src/main/java/ch/threema/client/ProtocolDefines.java
  59. 5 1
      app/src/main/java/ch/threema/storage/DatabaseServiceNew.java
  60. 8 2
      app/src/main/java/ch/threema/storage/factories/ContactModelFactory.java
  61. 54 7
      app/src/main/java/ch/threema/storage/models/ContactModel.java
  62. 1 1
      app/src/main/res/drawable/ic_new_badge_filled.xml
  63. 36 45
      app/src/main/res/layout/activity_storagemanagement.xml
  64. 2 0
      app/src/main/res/layout/fragment_contacts.xml
  65. 32 48
      app/src/main/res/layout/fragment_my_id.xml
  66. 1 1
      app/src/main/res/layout/fragment_wizard1.xml
  67. 1 1
      app/src/main/res/layout/fragment_wizard2.xml
  68. 45 6
      app/src/main/res/layout/header_contact_detail.xml
  69. 5 36
      app/src/main/res/layout/header_contact_section_work.xml
  70. 1 1
      app/src/main/res/layout/item_contact_list.xml
  71. 6 5
      app/src/main/res/layout/popup_identity.xml
  72. 14 5
      app/src/main/res/values-ca/strings.xml
  73. 34 23
      app/src/main/res/values-cs/strings.xml
  74. 10 3
      app/src/main/res/values-de/strings.xml
  75. 9 1
      app/src/main/res/values-es/strings.xml
  76. 9 1
      app/src/main/res/values-fr/strings.xml
  77. 1 2
      app/src/main/res/values-it/strings.xml
  78. 9 1
      app/src/main/res/values-nl-rNL/strings.xml
  79. 9 1
      app/src/main/res/values-pl/strings.xml
  80. 9 1
      app/src/main/res/values-pt-rBR/strings.xml
  81. 18 0
      app/src/main/res/values-rm/strings.xml
  82. 9 1
      app/src/main/res/values-ru/strings.xml
  83. 15 8
      app/src/main/res/values-tr/strings.xml
  84. 12 4
      app/src/main/res/values-zh-rCN/strings.xml
  85. 13 5
      app/src/main/res/values-zh-rTW/strings.xml
  86. 6 0
      app/src/main/res/values/arrays.xml
  87. 1 0
      app/src/main/res/values/dimens.xml
  88. 9 2
      app/src/main/res/values/strings.xml
  89. 9 0
      app/src/main/res/values/styles.xml
  90. 6 0
      app/src/main/res/xml/network_security_config.xml
  91. 9 3
      app/src/main/res/xml/preference_privacy.xml
  92. 38 0
      app/src/none/AndroidManifest.xml
  93. 39 0
      app/src/red/res/layout/header_contact_section_work.xml
  94. 0 10
      app/src/red/res/xml/backup_content.xml
  95. 13 7
      app/src/red/res/xml/preference_appearance.xml
  96. 39 0
      app/src/store_google_work/res/layout/header_contact_section_work.xml
  97. 29 0
      app/src/store_google_work/res/xml/app_shortcuts.xml
  98. 0 10
      app/src/store_google_work/res/xml/backup_content.xml
  99. 13 7
      app/src/store_google_work/res/xml/preference_appearance.xml
  100. 1 17
      app/src/test/java/ch/threema/client/Helpers.java

+ 13 - 14
app/build.gradle

@@ -11,8 +11,8 @@ if (getGradle().getStartParameter().getTaskRequests().toString().contains("Hms")
 }
 
 // version codes
-def app_version = "4.56"
-def beta_suffix = ""
+def app_version = "4.57"
+def beta_suffix = "" // with leading dash
 
 /**
  * Return the git hash, if git is installed.
@@ -86,7 +86,7 @@ android {
         vectorDrawables.useSupportLibrary = true
         applicationId "ch.threema.app"
         testApplicationId 'ch.threema.app.test'
-        versionCode 688
+        versionCode 694
         versionName "${app_version}${beta_suffix}"
         resValue "string", "app_name", "Threema"
         // package name used for sync adapter
@@ -524,22 +524,22 @@ dependencies {
     implementation 'org.slf4j:slf4j-api:1.7.30'
     implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.23'
     implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
-    implementation 'com.datatheorem.android.trustkit:trustkit:1.1.3'
+    implementation 'com.datatheorem.android.trustkit:trustkit:1.1.5'
     implementation 'com.takisoft.preferencex:preferencex:1.1.0'
     implementation 'me.zhanghai.android.fastscroll:library:1.1.5'
 
     // AndroidX / Jetpack support libraries
-    implementation "androidx.core:core:1.5.0"
+    implementation "androidx.core:core:1.6.0"
     implementation "androidx.preference:preference:1.1.1"
-    implementation 'androidx.legacy:legacy-support-v13:1.0.0'
     implementation 'androidx.recyclerview:recyclerview:1.2.1'
     implementation 'androidx.palette:palette:1.0.0'
-    implementation 'androidx.appcompat:appcompat:1.3.0'
-    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
+    implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
+    implementation 'androidx.appcompat:appcompat:1.3.1'
+    implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
     implementation 'androidx.biometric:biometric:1.1.0'
     implementation "androidx.work:work-runtime:2.5.0"
-    implementation 'androidx.fragment:fragment:1.3.5'
-    implementation 'androidx.activity:activity:1.2.3'
+    implementation 'androidx.fragment:fragment:1.3.6'
+    implementation 'androidx.activity:activity:1.2.4'
     implementation 'androidx.sqlite:sqlite:2.1.0'
     implementation "androidx.concurrent:concurrent-futures:1.1.0"
     implementation "androidx.camera:camera-camera2:1.0.1"
@@ -555,8 +555,7 @@ dependencies {
     implementation "androidx.lifecycle:lifecycle-process:2.3.1"
     implementation "androidx.lifecycle:lifecycle-common-java8:2.3.1"
     implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
-    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
-    implementation "androidx.paging:paging-runtime:3.0.0"
+    implementation "androidx.paging:paging-runtime:3.0.1"
 
     implementation 'com.google.android.material:material:1.4.0'
     implementation 'com.google.android.exoplayer:exoplayer-core:2.13.3'
@@ -586,8 +585,8 @@ dependencies {
     }
 
     // Glide components
-    implementation 'com.github.bumptech.glide:glide:4.11.0'
-    annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
+    implementation 'com.github.bumptech.glide:glide:4.12.0'
+    annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
 
     // use leak canary in debug builds
 //    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.5'

+ 1 - 1
app/src/google_services_based/java/ch/threema/app/wearable/WearableHandler.java

@@ -135,7 +135,7 @@ public class WearableHandler {
 			} catch (ExecutionException e) {
 				final String message = e.getMessage();
 				if (message != null && message.contains("Wearable.API is not available on this device")) {
-					logger.info("cancelOnWearable: ExecutionException while trying to connect to wearable: {}", message);
+					logger.debug("cancelOnWearable: ExecutionException while trying to connect to wearable: {}", message);
 				} else {
 					logger.info("cancelOnWearable: ExecutionException while trying to connect to wearable: {}", message);
 				}

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

@@ -380,7 +380,7 @@ public class ContactDetailActivity extends ThreemaToolbarActivity
 			}
 		});
 
-		if (preferenceService.isSyncContacts() && ContactUtil.isSynchronized(contact)) {
+		if (contact.getAndroidContactLookupKey() != null) {
 			floatingActionButton.setContentDescription(getString(R.string.edit));
 			floatingActionButton.setImageResource(R.drawable.ic_outline_contacts_app_24);
 		}
@@ -844,7 +844,7 @@ public class ContactDetailActivity extends ThreemaToolbarActivity
 		IdListService excludeFromSyncListService = this.serviceManager.getExcludedSyncIdentitiesService();
 
 		//second question, if the contact is a synced contact
-		if (ContactUtil.isSynchronized(contactModel) && excludeFromSyncListService != null
+		if (contactModel.getAndroidContactLookupKey() != null && excludeFromSyncListService != null
 				&& !excludeFromSyncListService.has(contactModel.getIdentity())) {
 
 			GenericAlertDialog dialogFragment = GenericAlertDialog.newInstance(

+ 63 - 71
app/src/main/java/ch/threema/app/activities/GroupDetailActivity.java

@@ -131,7 +131,6 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 	private String myIdentity;
 
 	private GroupModel groupModel;
-	private RecyclerView groupDetailRecyclerView;
 	private GroupDetailAdapter groupDetailAdapter;
 	private CollapsingToolbarLayout collapsingToolbar;
 	private ResumePauseHandler resumePauseHandler;
@@ -172,13 +171,13 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 		}
 	};
 
-	private class SelectorInfo {
+	private static class SelectorInfo {
 		public View view;
 		public ContactModel contactModel;
 		public ArrayList<Integer> optionsMap;
 	}
 
-	private ContactSettingsListener contactSettingsListener = new ContactSettingsListener() {
+	private final ContactSettingsListener contactSettingsListener = new ContactSettingsListener() {
 		@Override
 		public void onSortingChanged() { }
 
@@ -197,7 +196,7 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 		public void onNotificationSettingChanged(String uid) { }
 	};
 
-	private ContactListener contactListener = new ContactListener() {
+	private final ContactListener contactListener = new ContactListener() {
 		@Override
 		public void onModified(ContactModel modifiedContactModel) {
 			resumePauseHandler.runOnActive(RUN_ON_ACTIVE_RELOAD, runIfActiveUpdate);
@@ -214,7 +213,7 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 		}
 	};
 
-	private GroupListener groupListener = new GroupListener() {
+	private final GroupListener groupListener = new GroupListener() {
 		@Override
 		public void onCreate(GroupModel newGroupModel) {
 			resumePauseHandler.runOnActive(RUN_ON_ACTIVE_RELOAD, runIfActiveUpdate);
@@ -291,7 +290,7 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 		this.avatarEditView = findViewById(R.id.avatar_edit_view);
 		this.collapsingToolbar = findViewById(R.id.collapsing_toolbar);
 		this.floatingActionButton = findViewById(R.id.floating);
-		this.groupDetailRecyclerView = findViewById(R.id.group_members_list);
+		RecyclerView groupDetailRecyclerView = findViewById(R.id.group_members_list);
 		this.collapsingToolbar.setTitle(" ");
 		this.groupNameEditText = findViewById(R.id.group_title);
 
@@ -387,11 +386,11 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 			actionBar.setDisplayHomeAsUpEnabled(true);
 		}
 
-		this.groupDetailRecyclerView.setLayoutManager(new LinearLayoutManager(this));
+		groupDetailRecyclerView.setLayoutManager(new LinearLayoutManager(this));
 
 		setupAdapter();
 
-		this.groupDetailRecyclerView.setAdapter(this.groupDetailAdapter);
+		groupDetailRecyclerView.setAdapter(this.groupDetailAdapter);
 
 		final Observer<List<ContactModel>> groupMemberObserver = new Observer<List<ContactModel>>() {
 			@Override
@@ -530,9 +529,10 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 	}
 
 	private void sortGroupMembers() {
+		final boolean isSortingFirstName = preferenceService.isContactListSortingFirstName();
 		List<ContactModel> contactModels = groupDetailViewModel.getGroupContacts();
-		Collections.sort(contactModels, (model1, model2) -> ContactUtil.getSafeNameString(model1, preferenceService).compareTo(
-				ContactUtil.getSafeNameString(model2, preferenceService)
+		Collections.sort(contactModels, (model1, model2) -> ContactUtil.getSafeNameString(model1, isSortingFirstName).compareTo(
+				ContactUtil.getSafeNameString(model2, isSortingFirstName)
 		));
 		groupDetailViewModel.setGroupContacts(contactModels);
 	}
@@ -611,56 +611,50 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 
 	@Override
 	public boolean onOptionsItemSelected(MenuItem item) {
-		switch (item.getItemId()) {
-			case android.R.id.home:
-				finishUp();
-				return true;
-			case R.id.action_send_message:
-				if (groupModel != null) {
-					Intent intent = new Intent(this, ComposeMessageActivity.class);
-					intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-					intent.putExtra(ThreemaApplication.INTENT_DATA_GROUP, groupId);
-					intent.putExtra(ThreemaApplication.INTENT_DATA_EDITFOCUS, Boolean.TRUE);
-					startActivity(intent);
-					finish();
-				}
-				break;
-			case R.id.menu_resync:
-				this.syncGroup();
-				break;
-			case R.id.menu_leave_group:
-				int leaveMessageRes = operationMode == MODE_READONLY ? R.string.really_leave_group_message : R.string.really_leave_group_admin_message;
+		int itemId = item.getItemId();
+		if (itemId == android.R.id.home) {
+			finishUp();
+			return true;
+		} else if (itemId == R.id.action_send_message) {
+			if (groupModel != null) {
+				Intent intent = new Intent(this, ComposeMessageActivity.class);
+				intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+				intent.putExtra(ThreemaApplication.INTENT_DATA_GROUP, groupId);
+				intent.putExtra(ThreemaApplication.INTENT_DATA_EDITFOCUS, Boolean.TRUE);
+				startActivity(intent);
+				finish();
+			}
+		} else if (itemId == R.id.menu_resync) {
+			this.syncGroup();
+		} else if (itemId == R.id.menu_leave_group) {
+			int leaveMessageRes = operationMode == MODE_READONLY ? R.string.really_leave_group_message : R.string.really_leave_group_admin_message;
 
-				GenericAlertDialog.newInstance(
-						R.string.action_leave_group,
-						Html.fromHtml(getString(leaveMessageRes)),
-						R.string.ok,
-						R.string.cancel)
-						.show(getSupportFragmentManager(), DIALOG_TAG_LEAVE_GROUP);
-				break;
-			case R.id.menu_delete_group:
-				GenericAlertDialog.newInstance(
-						R.string.action_delete_group,
-						groupService.isGroupOwner(groupModel) ? R.string.delete_my_group_message : R.string.delete_group_message,
-						R.string.ok,
-						R.string.cancel)
-						.show(getSupportFragmentManager(), DIALOG_TAG_DELETE_GROUP);
-				break;
-			case R.id.menu_gallery:
-				if (groupId > 0 && !hiddenChatsListService.has(groupService.getUniqueIdString(this.groupModel))) {
-					Intent mediaGalleryIntent = new Intent(this, MediaGalleryActivity.class);
-					mediaGalleryIntent.putExtra(ThreemaApplication.INTENT_DATA_GROUP, groupId);
-					startActivity(mediaGalleryIntent);
-				}
-				break;
-			case R.id.menu_clone_group:
-				GenericAlertDialog.newInstance(
-						R.string.action_clone_group,
-						R.string.clone_group_message,
-						R.string.yes,
-						R.string.no)
-						.show(getSupportFragmentManager(), DIALOG_TAG_CLONE_GROUP_CONFIRM);
-				break;
+			GenericAlertDialog.newInstance(
+				R.string.action_leave_group,
+				Html.fromHtml(getString(leaveMessageRes)),
+				R.string.ok,
+				R.string.cancel)
+				.show(getSupportFragmentManager(), DIALOG_TAG_LEAVE_GROUP);
+		} else if (itemId == R.id.menu_delete_group) {
+			GenericAlertDialog.newInstance(
+				R.string.action_delete_group,
+				groupService.isGroupOwner(groupModel) ? R.string.delete_my_group_message : R.string.delete_group_message,
+				R.string.ok,
+				R.string.cancel)
+				.show(getSupportFragmentManager(), DIALOG_TAG_DELETE_GROUP);
+		} else if (itemId == R.id.menu_gallery) {
+			if (groupId > 0 && !hiddenChatsListService.has(groupService.getUniqueIdString(this.groupModel))) {
+				Intent mediaGalleryIntent = new Intent(this, MediaGalleryActivity.class);
+				mediaGalleryIntent.putExtra(ThreemaApplication.INTENT_DATA_GROUP, groupId);
+				startActivity(mediaGalleryIntent);
+			}
+		} else if (itemId == R.id.menu_clone_group) {
+			GenericAlertDialog.newInstance(
+				R.string.action_clone_group,
+				R.string.clone_group_message,
+				R.string.yes,
+				R.string.no)
+				.show(getSupportFragmentManager(), DIALOG_TAG_CLONE_GROUP_CONFIRM);
 		}
 		return super.onOptionsItemSelected(item);
 	}
@@ -808,18 +802,16 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 	@Override
 	public void onActivityResult(int requestCode, int resultCode, Intent data) {
 		if (resultCode == Activity.RESULT_OK) {
-			switch (requestCode) {
-				case ThreemaActivity.ACTIVITY_ID_GROUP_ADD:
-					// some users were added
-					groupDetailViewModel.addGroupContacts(IntentDataUtil.getContactIdentities(data));
-					sortGroupMembers();
-					this.hasChanges = true;
-					break;
-				default:
-					if (this.avatarEditView != null) {
-						this.avatarEditView.onActivityResult(requestCode, resultCode, data);
-					}
-					super.onActivityResult(requestCode, resultCode, data);
+			if (requestCode == ThreemaActivity.ACTIVITY_ID_GROUP_ADD) {
+				// some users were added
+				groupDetailViewModel.addGroupContacts(IntentDataUtil.getContactIdentities(data));
+				sortGroupMembers();
+				this.hasChanges = true;
+			} else {
+				if (this.avatarEditView != null) {
+					this.avatarEditView.onActivityResult(requestCode, resultCode, data);
+				}
+				super.onActivityResult(requestCode, resultCode, data);
 			}
 		}
 

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

@@ -374,15 +374,18 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
 	};
 
 	private void updateBottomNavigation() {
-		RuntimeUtil.runOnUiThread(() -> {
-			try {
-				new UpdateBottomNavigationBadgeTask(HomeActivity.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
-			} catch (RejectedExecutionException e) {
+		if (preferenceService.getShowUnreadBadge()) {
+			RuntimeUtil.runOnUiThread(() -> {
 				try {
-					new UpdateBottomNavigationBadgeTask(HomeActivity.this).executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
-				} catch (RejectedExecutionException ignored) {}
-			}
-		});
+					new UpdateBottomNavigationBadgeTask(HomeActivity.this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+				} catch (RejectedExecutionException e) {
+					try {
+						new UpdateBottomNavigationBadgeTask(HomeActivity.this).executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
+					} catch (RejectedExecutionException ignored) {
+					}
+				}
+			});
+		}
 	}
 
 	private final ConversationListener conversationListener = new ConversationListener() {
@@ -499,6 +502,7 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
 						BottomNavigationView bottomNavigationView = findViewById(R.id.bottom_navigation);
 						if (bottomNavigationView != null) {
 							BadgeDrawable badgeDrawable = bottomNavigationView.getOrCreateBadge(R.id.contacts);
+							badgeDrawable.setBackgroundColor(ConfigUtils.getAccentColor(HomeActivity.this));
 							if (badgeDrawable.getVerticalOffset() == 0) {
 								badgeDrawable.setVerticalOffset(getResources().getDimensionPixelSize(R.dimen.bottom_nav_badge_offset_vertical));
 							}
@@ -1119,9 +1123,7 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
 			bottomNavigationView.setSelectedItemId(initialItemId);
 		});
 
-		if (preferenceService.getShowUnreadBadge()) {
-			new UpdateBottomNavigationBadgeTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
-		}
+		updateBottomNavigation();
 
 		// restore sync adapter account if necessary
 		if (preferenceService.isSyncContacts()) {

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

@@ -46,6 +46,8 @@ import androidx.annotation.StringRes;
 import androidx.appcompat.app.ActionBar;
 import androidx.appcompat.widget.SearchView;
 import androidx.appcompat.widget.Toolbar;
+import androidx.core.view.WindowInsetsCompat;
+import androidx.core.view.WindowInsetsControllerCompat;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentManager;
 import androidx.fragment.app.FragmentPagerAdapter;
@@ -79,7 +81,7 @@ abstract public class MemberChooseActivity extends ThreemaToolbarActivity implem
 	protected ArrayList<String> preselectedIdentities = new ArrayList<>();
 
 	private ViewPager viewPager;
-	private ArrayList<Integer> tabs = new ArrayList<>(NUM_FRAGMENTS);
+	private final ArrayList<Integer> tabs = new ArrayList<>(NUM_FRAGMENTS);
 	private Snackbar snackbar;
 	private View rootView;
 
@@ -247,6 +249,9 @@ abstract public class MemberChooseActivity extends ThreemaToolbarActivity implem
 				RuntimeUtil.runOnUiThread(new Runnable() {
 					@Override
 					public void run() {
+						if (searchView != null) {
+							new WindowInsetsControllerCompat(getWindow(), searchView).hide(WindowInsetsCompat.Type.ime());
+						}
 						menuNext(getSelectedContacts());
 					}
 				});

+ 9 - 10
app/src/main/java/ch/threema/app/activities/RecipientListBaseActivity.java

@@ -654,20 +654,19 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 		return null;
 	}
 
-	private String getTextFromIntent(Intent intent) {
+	private @Nullable String getTextFromIntent(Intent intent) {
 		String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
 		String text = intent.getStringExtra(Intent.EXTRA_TEXT);
-		String textIntent;
-		if (subject != null && subject.length() > 0 && !subject.equals(text)) {
-			textIntent = subject;
-			if (!TextUtils.isEmpty(text)) {
-				textIntent += " - " + text;
-			}
+
+		if (TestUtil.empty(text)) {
+			return subject;
 		}
-		else {
-			textIntent = text;
+
+		if (TestUtil.empty(subject) || text.startsWith(subject)) {
+			return text;
 		}
-		return textIntent;
+
+		return subject + " - " + text;
 	}
 
 	private void addMediaItem(String mimeType, @NonNull Uri uri, @Nullable String caption) {

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

@@ -211,8 +211,8 @@ public class SendMediaActivity extends ThreemaToolbarActivity implements
 					@Override
 					public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
 
-						logger.info("%%% system window top " + insets.getSystemWindowInsetTop() + " bottom " + insets.getSystemWindowInsetBottom());
-						logger.info("%%% stable insets top " + insets.getStableInsetTop() + " bottom " + insets.getStableInsetBottom());
+						logger.debug("system window top " + insets.getSystemWindowInsetTop() + " bottom " + insets.getSystemWindowInsetBottom());
+						logger.debug("stable insets top " + insets.getStableInsetTop() + " bottom " + insets.getStableInsetBottom());
 
 						if (insets.getSystemWindowInsetBottom() <= insets.getStableInsetBottom()) {
 							onSoftKeyboardClosed();

+ 10 - 29
app/src/main/java/ch/threema/app/activities/StorageManagementActivity.java

@@ -28,15 +28,14 @@ import android.text.format.Formatter;
 import android.view.Gravity;
 import android.view.MenuItem;
 import android.view.View;
-import android.widget.AdapterView;
 import android.widget.ArrayAdapter;
 import android.widget.Button;
 import android.widget.FrameLayout;
 import android.widget.ProgressBar;
-import android.widget.Spinner;
 import android.widget.TextView;
 
 import com.google.android.material.snackbar.Snackbar;
+import com.google.android.material.textfield.MaterialAutoCompleteTextView;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -78,7 +77,7 @@ public class StorageManagementActivity extends ThreemaToolbarActivity implements
 	private MessageService messageService;
 	private ConversationService conversationService;
 	private TextView totalView, usageView, freeView, messageView, inuseView;
-	private Spinner timeSpinner, messageTimeSpinner;
+	private MaterialAutoCompleteTextView timeSpinner, messageTimeSpinner;
 	private Button deleteButton, messageDeleteButton;
 	private ProgressBar progressBar;
 	private boolean isCancelled, isMessageDeleteCancelled;
@@ -156,36 +155,18 @@ public class StorageManagementActivity extends ThreemaToolbarActivity implements
 			}
 		});
 
-		ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
-				R.array.storagemanager_timeout, android.R.layout.simple_spinner_item);
-		adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+		final ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.storagemanager_timeout, android.R.layout.simple_spinner_dropdown_item);
 		timeSpinner.setAdapter(adapter);
-		timeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
-			@Override
-			public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
-				selectedSpinnerItem = position;
-			}
-
-			@Override
-			public void onNothingSelected(AdapterView<?> parent) {
-				selectedSpinnerItem = 0;
-			}
+		timeSpinner.setText(adapter.getItem(selectedSpinnerItem), false);
+		timeSpinner.setOnItemClickListener((parent, view, position, id) -> {
+			selectedSpinnerItem = position;
 		});
 
-		ArrayAdapter<CharSequence> messageCleanupAdapter = ArrayAdapter.createFromResource(this,
-				R.array.storagemanager_timeout, android.R.layout.simple_spinner_item);
-		messageCleanupAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+		final ArrayAdapter<CharSequence> messageCleanupAdapter = ArrayAdapter.createFromResource(this, R.array.storagemanager_timeout, android.R.layout.simple_spinner_dropdown_item);
 		messageTimeSpinner.setAdapter(messageCleanupAdapter);
-		messageTimeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
-			@Override
-			public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
-				selectedMessageSpinnerItem = position;
-			}
-
-			@Override
-			public void onNothingSelected(AdapterView<?> parent) {
-				selectedMessageSpinnerItem = 0;
-			}
+		messageTimeSpinner.setText(messageCleanupAdapter.getItem(selectedMessageSpinnerItem), false);
+		messageTimeSpinner.setOnItemClickListener((parent, view, position, id) -> {
+			selectedMessageSpinnerItem = position;
 		});
 
 		storageFull.post(new Runnable() {

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

@@ -282,10 +282,10 @@ public abstract class ThreemaToolbarActivity extends ThreemaActivity implements
 	}
 
 	public void onSoftKeyboardOpened(int softKeyboardHeight) {
-		logger.info("%%% Potential keyboard height = " + softKeyboardHeight + " Min = " + minKeyboardHeight);
+		logger.debug("Potential keyboard height = " + softKeyboardHeight + " Min = " + minKeyboardHeight);
 
 		if (softKeyboardHeight >= minKeyboardHeight) {
-			logger.info("%%% Soft keyboard open detected");
+			logger.debug("Soft keyboard open detected");
 
 			this.softKeyboardOpen = true;
 			saveSoftKeyboardHeight(softKeyboardHeight);
@@ -295,7 +295,7 @@ public abstract class ThreemaToolbarActivity extends ThreemaActivity implements
 	}
 
 	public void onSoftKeyboardClosed() {
-		logger.info("%%% Soft keyboard closed");
+		logger.debug("Soft keyboard closed");
 
 		this.softKeyboardOpen = false;
 
@@ -353,11 +353,11 @@ public abstract class ThreemaToolbarActivity extends ThreemaActivity implements
 
 	public void saveSoftKeyboardHeight(int softKeyboardHeight) {
 		if (ConfigUtils.isLandscape(this)) {
-			logger.info("%%% Keyboard height (landscape): " + softKeyboardHeight);
+			logger.info("Keyboard height (landscape): " + softKeyboardHeight);
 			PreferenceManager.getDefaultSharedPreferences(this)
 				.edit().putInt(LANDSCAPE_HEIGHT, softKeyboardHeight).apply();
 		} else {
-			logger.info("%%% Keyboard height (portrait): " + softKeyboardHeight);
+			logger.info("Keyboard height (portrait): " + softKeyboardHeight);
 			PreferenceManager.getDefaultSharedPreferences(this)
 				.edit().putInt(PORTRAIT_HEIGHT, softKeyboardHeight).apply();
 		}

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

@@ -60,6 +60,7 @@ import ch.threema.app.dialogs.WizardDialog;
 import ch.threema.app.exceptions.EntryAlreadyExistsException;
 import ch.threema.app.exceptions.FileSystemNotPresentException;
 import ch.threema.app.exceptions.InvalidEntryException;
+import ch.threema.app.exceptions.PolicyViolationException;
 import ch.threema.app.fragments.wizard.WizardFragment0;
 import ch.threema.app.fragments.wizard.WizardFragment1;
 import ch.threema.app.fragments.wizard.WizardFragment2;
@@ -85,7 +86,6 @@ import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.TextUtil;
 import ch.threema.app.workers.IdentityStatesWorker;
-import ch.threema.app.exceptions.PolicyViolationException;
 import ch.threema.client.LinkEmailException;
 import ch.threema.client.LinkMobileNoException;
 import ch.threema.localcrypto.MasterKeyLockedException;
@@ -138,7 +138,7 @@ public class WizardBaseActivity extends ThreemaAppCompatActivity implements View
 	private final Handler finishHandler = new Handler();
 	private final Handler dialogHandler = new Handler();
 
-	private Runnable finishTask = new Runnable() {
+	private final Runnable finishTask = new Runnable() {
 		@Override
 		public void run() {
 		 	RuntimeUtil.runOnUiThread(new Runnable() {

+ 34 - 2
app/src/main/java/ch/threema/app/adapters/ContactDetailAdapter.java

@@ -28,11 +28,14 @@ import android.graphics.drawable.Drawable;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
 import android.widget.CheckBox;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import com.google.android.material.textfield.MaterialAutoCompleteTextView;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -43,6 +46,7 @@ import androidx.recyclerview.widget.RecyclerView;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.managers.ServiceManager;
+import ch.threema.app.services.ContactService;
 import ch.threema.app.services.FingerPrintService;
 import ch.threema.app.services.GroupService;
 import ch.threema.app.services.IdListService;
@@ -50,7 +54,6 @@ import ch.threema.app.services.PreferenceService;
 import ch.threema.app.ui.VerificationLevelImageView;
 import ch.threema.app.utils.AndroidContactUtil;
 import ch.threema.app.utils.ConfigUtils;
-import ch.threema.app.utils.ContactUtil;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.GroupModel;
 
@@ -61,6 +64,7 @@ public class ContactDetailAdapter extends RecyclerView.Adapter<RecyclerView.View
 	private static final int TYPE_ITEM = 1;
 
 	private final Context context;
+	private ContactService contactService;
 	private GroupService groupService;
 	private PreferenceService preferenceService;
 	private FingerPrintService fingerprintService;
@@ -92,6 +96,7 @@ public class ContactDetailAdapter extends RecyclerView.Adapter<RecyclerView.View
 		private final ImageView syncSourceIcon;
 		private final TextView publicNickNameView;
 		private final LinearLayout groupMembershipTitle;
+		private final MaterialAutoCompleteTextView readReceiptsSpinner, typingIndicatorsSpinner;
 
 		public HeaderHolder(View view) {
 			super(view);
@@ -107,6 +112,8 @@ public class ContactDetailAdapter extends RecyclerView.Adapter<RecyclerView.View
 			this.publicNickNameView = itemView.findViewById(R.id.public_nickname);
 			this.groupMembershipTitle = itemView.findViewById(R.id.group_members_title_container);
 			this.syncSourceIcon = itemView.findViewById(R.id.sync_source_icon);
+			this.readReceiptsSpinner = itemView.findViewById(R.id.read_receipts_spinner);
+			this.typingIndicatorsSpinner = itemView.findViewById(R.id.typing_indicators_spinner);
 
 			verificationLevelIconView.setOnClickListener(new View.OnClickListener() {
 				@Override
@@ -126,6 +133,7 @@ public class ContactDetailAdapter extends RecyclerView.Adapter<RecyclerView.View
 		ServiceManager serviceManager = ThreemaApplication.getServiceManager();
 
 		try {
+			this.contactService = serviceManager.getContactService();
 			this.groupService = serviceManager.getGroupService();
 			this.fingerprintService = serviceManager.getFingerPrintService();
 			this.excludeFromSyncListService = serviceManager.getExcludedSyncIdentitiesService();
@@ -193,7 +201,7 @@ public class ContactDetailAdapter extends RecyclerView.Adapter<RecyclerView.View
 			headerHolder.verificationLevelImageView.setContactModel(contactModel);
 			headerHolder.verificationLevelImageView.setVisibility(View.VISIBLE);
 
-			if (preferenceService.isSyncContacts() && ContactUtil.isSynchronized(contactModel) &&
+			if (preferenceService.isSyncContacts() && contactModel.getAndroidContactLookupKey() != null &&
 				ConfigUtils.isPermissionGranted(ThreemaApplication.getAppContext(), Manifest.permission.READ_CONTACTS)) {
 				headerHolder.synchronizeContainer.setVisibility(View.VISIBLE);
 
@@ -229,6 +237,30 @@ public class ContactDetailAdapter extends RecyclerView.Adapter<RecyclerView.View
 			} else {
 				headerHolder.groupMembershipTitle.setVisibility(View.GONE);
 			}
+
+			final String[] choices = context.getResources().getStringArray(R.array.receipts_override_choices);
+			choices[0] = context.getString(R.string.receipts_override_choice_default,
+					choices[preferenceService.isReadReceipts() ? 1 : 2]);
+
+			ArrayAdapter<String> readReceiptsAdapter = new ArrayAdapter<>(context, android.R.layout.simple_spinner_dropdown_item, choices);
+			headerHolder.readReceiptsSpinner.setAdapter(readReceiptsAdapter);
+			headerHolder.readReceiptsSpinner.setText(choices[contactModel.getReadReceipts()], false);
+			headerHolder.readReceiptsSpinner.setOnItemClickListener((parent, view, position1, id) -> {
+				contactModel.setReadReceipts(position1);
+				contactService.save(contactModel);
+			});
+
+			final String[] typingChoices = context.getResources().getStringArray(R.array.receipts_override_choices);
+			typingChoices[0] = context.getString(R.string.receipts_override_choice_default,
+				typingChoices[preferenceService.isTypingIndicator() ? 1 : 2]);
+
+			ArrayAdapter<String> typingIndicatorAdapter = new ArrayAdapter<>(context, android.R.layout.simple_spinner_dropdown_item, typingChoices);
+			headerHolder.typingIndicatorsSpinner.setAdapter(typingIndicatorAdapter);
+			headerHolder.typingIndicatorsSpinner.setText(typingChoices[contactModel.getTypingIndicators()], false);
+			headerHolder.typingIndicatorsSpinner.setOnItemClickListener((parent, view, position12, id) -> {
+				contactModel.setTypingIndicators(position12);
+				contactService.save(contactModel);
+			});
 		}
 	}
 

+ 18 - 10
app/src/main/java/ch/threema/app/adapters/ContactListAdapter.java

@@ -122,7 +122,7 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
 		Date recentlyAddedDate = new Date(System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS);
 
 		for (ContactModel contactModel : all) {
-			if (contactModel != null && contactModel.getDateCreated() != null && recentlyAddedDate.before(contactModel.getDateCreated()) && !ContactUtil.isChannelContact(contactModel)) {
+			if (contactModel != null && contactModel.getDateCreated() != null && recentlyAddedDate.before(contactModel.getDateCreated()) && !ContactUtil.isChannelContact(contactModel) && !"ECHOECHO".equalsIgnoreCase(contactModel.getIdentity())) {
 				recents.add(contactModel);
 			}
 		}
@@ -132,7 +132,6 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
 			Collections.sort(recents, (o1, o2) -> o2.getDateCreated().compareTo(o1.getDateCreated()));
 			this.recentlyAdded = recents.subList(0, Math.min(recents.size() , MAX_RECENTLY_ADDED_CONTACTS));
 
-			all.removeAll(this.recentlyAdded);
 			all.addAll(0, this.recentlyAdded);
 		} else {
 			this.recentlyAdded.clear();
@@ -192,7 +191,7 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
 				continue;
 			}
 
-			firstLetter = getInitial(c, false);
+			firstLetter = getInitial(c, false, i);
 
 			if (PLACEHOLDER_BLANK_HEADER.equals(firstLetter) ||
 					PLACEHOLDER_CHANNELS.equals(firstLetter) ||
@@ -215,7 +214,7 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
 		ArrayList<String> sectionList = new ArrayList<String>(alphaIndexer.keySet());
 		Collections.sort(sectionList, collator);
 		if (sectionList.contains(PLACEHOLDER_CHANNELS)) {
-			// replace channels placeholder by copyright sign AFTER sorting
+			// replace channels placeholder by star sign AFTER sorting
 			sectionList.set(sectionList.indexOf(PLACEHOLDER_CHANNELS), CHANNEL_SIGN);
 			if (alphaIndexer.containsKey(PLACEHOLDER_CHANNELS)) {
 				alphaIndexer.put(CHANNEL_SIGN, alphaIndexer.get(PLACEHOLDER_CHANNELS));
@@ -232,16 +231,25 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
 		countsList.toArray(counts);
 	}
 
-	private String getInitial(ContactModel c, boolean afterSorting) {
+	private String getInitial(ContactModel c, boolean afterSorting, int position) {
 		String firstLetter, sortingValue;
 
-		sortingValue = ContactUtil.getSafeNameStringNoNickname(c, preferenceService);
+		sortingValue = ContactUtil.getSafeNameString(c, preferenceService.isContactListSortingFirstName());
 		if (sortingValue.length() == 0) {
 			firstLetter = PLACEHOLDER_BLANK_HEADER;
 		} else {
 			if (ContactUtil.isChannelContact(c)) {
 				firstLetter = afterSorting ? CHANNEL_SIGN : PLACEHOLDER_CHANNELS;
-			} else if (recentlyAdded != null && recentlyAdded.size() > 0 && recentlyAdded.contains(c)) {
+			} else if (recentlyAdded != null && recentlyAdded.size() > 0 && position < recentlyAdded.size() && recentlyAdded.contains(c)) {
+				if (contactListFilter != null && contactListFilter.getFilterString() != null) {
+					if (position > 0) {
+						for (int i = Math.min(position - 1, MAX_RECENTLY_ADDED_CONTACTS - 1) ; i >= 0; i--) {
+							if (values.get(i).equals(values.get(position))) {
+								return getIndexCharacter(sortingValue);
+							}
+						}
+					}
+				}
 				firstLetter = afterSorting ? RECENTLY_ADDED_SIGN : PLACEHOLDER_RECENTLY_ADDED;
 			} else {
 				firstLetter = getIndexCharacter(sortingValue);
@@ -358,9 +366,9 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
 				holder);
 
 		String previousInitial = PLACEHOLDER_CHANNELS;
-		String currentInitial = getInitial(contactModel, true);
+		String currentInitial = getInitial(contactModel, true, position);
 		if (position > 0) {
-			previousInitial = getInitial(values.get(position - 1), true);
+			previousInitial = getInitial(values.get(position - 1), true, position - 1);
 		}
 		if (previousInitial != null && !previousInitial.equals(currentInitial)) {
 			if (RECENTLY_ADDED_SIGN.equals(currentInitial)) {
@@ -520,7 +528,7 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
 
 	public String getInitial(int position) {
 		if (position < values.size() && position > 0) {
-			return getInitial(values.get(position), true);
+			return getInitial(values.get(position), true, position);
 		}
 		return "";
 	}

+ 3 - 1
app/src/main/java/ch/threema/app/dialogs/MessageDetailDialog.java

@@ -138,7 +138,9 @@ public class MessageDetailDialog extends ThreemaDialogFragment {
 					if (messageState != MessageState.SENT && !(messageModel.getType() == MessageType.BALLOT && messageModel instanceof GroupMessageModel)) {
 						Date modifiedAt = messageModel.getModifiedAt();
 						modifiedText.setText(TextUtil.capitalize(getString(stateResource)));
-						modifiedDate.setText(modifiedAt != null ? LocaleUtil.formatTimeStampStringAbsolute(getContext(), messageModel.getModifiedAt().getTime()) : "");
+						modifiedDate.setText(modifiedAt != null ?
+							LocaleUtil.formatTimeStampStringAbsolute(getContext(), messageModel.getModifiedAt().getTime()) :
+							(messageModel.getPostedAt(true) != null ? LocaleUtil.formatTimeStampStringAbsolute(getContext(), messageModel.getPostedAt(true).getTime()) : ""));
 						modifiedText.setVisibility(View.VISIBLE);
 						modifiedDate.setVisibility(View.VISIBLE);
 					}

+ 8 - 5
app/src/main/java/ch/threema/app/fragments/ComposeMessageFragment.java

@@ -1155,8 +1155,8 @@ public class ComposeMessageFragment extends Fragment implements
 						@Override
 						public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
 
-							logger.info("%%% system window top " + insets.getSystemWindowInsetTop() + " bottom " + insets.getSystemWindowInsetBottom());
-							logger.info("%%% stable insets top " + insets.getStableInsetTop() + " bottom " + insets.getStableInsetBottom());
+							logger.debug("system window top " + insets.getSystemWindowInsetTop() + " bottom " + insets.getSystemWindowInsetBottom());
+							logger.debug("stable insets top " + insets.getStableInsetTop() + " bottom " + insets.getStableInsetBottom());
 
 							if (insets.getSystemWindowInsetBottom() <= insets.getStableInsetBottom()) {
 								activity.onSoftKeyboardClosed();
@@ -1477,6 +1477,8 @@ public class ComposeMessageFragment extends Fragment implements
 							}
 						}
 
+						lastFirstVisibleItem = firstVisibleItem;
+
 						if (dateView.getVisibility() != View.VISIBLE && composeMessageAdapter != null && composeMessageAdapter.getCount() > 0) {
 							AnimationUtil.slideInAnimation(dateView, false, 200);
 						}
@@ -1484,14 +1486,15 @@ public class ComposeMessageFragment extends Fragment implements
 						dateViewHandler.removeCallbacks(dateViewTask);
 						dateViewHandler.postDelayed(dateViewTask, SCROLLBUTTON_VIEW_TIMEOUT);
 
-						lastFirstVisibleItem = firstVisibleItem;
 						if (composeMessageAdapter != null) {
 							AbstractMessageModel abstractMessageModel = composeMessageAdapter.getItem(firstVisibleItem);
 							if (abstractMessageModel != null) {
-								Date createdAt = abstractMessageModel.getCreatedAt();
+								final Date createdAt = abstractMessageModel.getCreatedAt();
 								if (createdAt != null) {
+									final String text = LocaleUtil.formatDateRelative(getActivity(), createdAt.getTime());
+									dateTextView.setText(text);
 									dateView.post(() -> {
-										dateTextView.setText(LocaleUtil.formatDateRelative(getActivity(), createdAt.getTime()));
+										dateTextView.setText(text);
 									});
 								}
 							}

+ 40 - 25
app/src/main/java/ch/threema/app/fragments/ContactsSectionFragment.java

@@ -171,6 +171,10 @@ public class ContactsSectionFragment
 				return;
 			}
 
+			if (actionMode != null) {
+				actionMode.finish();
+			}
+
 			new FetchContactsTask(contactService, false, tab.getPosition(), true) {
 				@Override
 				protected void onPostExecute(Pair<List<ContactModel>, FetchResults> result) {
@@ -215,10 +219,6 @@ public class ContactsSectionFragment
 	private void startSwipeRefresh() {
 		if (swipeRefreshLayout != null) {
 			swipeRefreshLayout.setRefreshing(true);
-
-			if (ConfigUtils.isWorkBuild() && workTabLayout != null) {
-				workTabLayout.selectTab(workTabLayout.getTabAt(TAB_ALL_CONTACTS), true);
-			}
 		}
 	}
 
@@ -345,7 +345,7 @@ public class ContactsSectionFragment
 	private final ContactListener contactListener = new ContactListener() {
 		@Override
 		public void onModified(ContactModel modifiedContactModel) {
-			logger.debug("*** onModified " + modifiedContactModel.getIdentity());
+			logger.debug("onModified " + modifiedContactModel.getIdentity());
 			if (resumePauseHandler != null) {
 				resumePauseHandler.runOnActive(RUN_ON_ACTIVE_UPDATE_LIST, runIfActiveUpdateList);
 			}
@@ -353,7 +353,7 @@ public class ContactsSectionFragment
 
 		@Override
 		public void onAvatarChanged(ContactModel contactModel) {
-			logger.debug("*** onAvatarChanged -> onModified " + contactModel.getIdentity());
+			logger.debug("onAvatarChanged -> onModified " + contactModel.getIdentity());
 			this.onModified(contactModel);
 		}
 
@@ -421,10 +421,12 @@ public class ContactsSectionFragment
 			// Count new contacts
 			final FetchResults results = new FetchResults();
 
-			if (ConfigUtils.isWorkBuild() && selectedTab == TAB_WORK_ONLY) {
+			if (ConfigUtils.isWorkBuild()) {
 				results.workCount = contactService.countIsWork();
-				if (results.workCount > 0 || forceWork) {
-					allContacts = contactService.getIsWork();
+				if (selectedTab == TAB_WORK_ONLY) {
+					if (results.workCount > 0 || forceWork) {
+						allContacts = contactService.getIsWork();
+					}
 				}
 			}
 
@@ -455,7 +457,7 @@ public class ContactsSectionFragment
 
 	@Override
 	public void onResume() {
-		logger.debug("*** onResume");
+		logger.debug("onResume");
 		if (this.resumePauseHandler != null) {
 			this.resumePauseHandler.onResume();
 		}
@@ -470,7 +472,7 @@ public class ContactsSectionFragment
 	@Override
 	public void onPause() {
 		super.onPause();
-		logger.debug("*** onPause");
+		logger.debug("onPause");
 
 		if (this.resumePauseHandler != null) {
 			this.resumePauseHandler.onPause();
@@ -480,7 +482,7 @@ public class ContactsSectionFragment
 	@Override
 	public void onCreate(Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
-		logger.debug("*** onCreate");
+		logger.debug("onCreate");
 
 		setRetainInstance(true);
 		setHasOptionsMenu(true);
@@ -497,12 +499,12 @@ public class ContactsSectionFragment
 	@Override
 	public void onAttach(@NonNull Activity activity) {
 		super.onAttach(activity);
-		logger.debug("*** onAttach");
+		logger.debug("onAttach");
 	}
 
 	@Override
 	public void onDestroy() {
-		logger.debug("*** onDestroy");
+		logger.debug("onDestroy");
 
 		removeListeners();
 
@@ -515,7 +517,7 @@ public class ContactsSectionFragment
 
 	@Override
 	public void onHiddenChanged(boolean hidden) {
-		logger.debug("*** onHiddenChanged: " + hidden);
+		logger.debug("onHiddenChanged: " + hidden);
 		if (hidden) {
 			if (actionMode != null) {
 				actionMode.finish();
@@ -539,7 +541,7 @@ public class ContactsSectionFragment
 		super.onPrepareOptionsMenu(menu);
 
 		// move search item to popup if the lock item is visible
-		if (lockAppService.isLockingEnabled()) {
+		if (lockAppService != null && lockAppService.isLockingEnabled()) {
 			this.searchMenuItem.setShowAsAction(SHOW_AS_ACTION_NEVER | SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
 		} else {
 			this.searchMenuItem.setShowAsAction(SHOW_AS_ACTION_ALWAYS | SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
@@ -548,7 +550,7 @@ public class ContactsSectionFragment
 
 	@Override
 	public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
-		logger.debug("*** onCreateOptionsMenu");
+		logger.debug("onCreateOptionsMenu");
 		searchMenuItem = menu.findItem(R.id.menu_search_contacts);
 
 		if (searchMenuItem == null) {
@@ -704,6 +706,22 @@ public class ContactsSectionFragment
 					contactsCounterChip.setVisibility(View.GONE);
 				}
 			}
+			if (ConfigUtils.isWorkBuild() && counts != null) {
+				if (workTabLayout != null) {
+					FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) listView.getLayoutParams();
+					if (counts.workCount > 0) {
+						workTabLayout.setVisibility(View.VISIBLE);
+						layoutParams.topMargin = getResources().getDimensionPixelSize(R.dimen.header_contact_section_work_height);
+					} else {
+						if (workTabLayout.getSelectedTabPosition() != 0) {
+							workTabLayout.selectTab(workTabLayout.getTabAt(0));
+						}
+						workTabLayout.setVisibility(View.GONE);
+						layoutParams.topMargin = 0;
+					}
+					listView.setLayoutParams(layoutParams);
+				}
+			}
 		}
 	}
 
@@ -751,7 +769,7 @@ public class ContactsSectionFragment
 	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
 		View headerView, fragmentView = getView();
 
-		logger.debug("*** onCreateView");
+		logger.debug("onCreateView");
 		if (fragmentView == null) {
 			fragmentView = inflater.inflate(R.layout.fragment_contacts, container, false);
 
@@ -833,10 +851,7 @@ public class ContactsSectionFragment
 					}
 				});
 			} else {
-				headerView = View.inflate(getActivity(), R.layout.header_contact_section_work, null);
-				listView.addHeaderView(headerView, null, false);
-
-				workTabLayout = ((TabLayout) headerView.findViewById(R.id.tab_layout));
+				workTabLayout = fragmentView.findViewById(R.id.work_contacts_tab_layout);
 				workTabLayout.addOnTabSelectedListener(onTabSelectedListener);
 			}
 
@@ -864,7 +879,7 @@ public class ContactsSectionFragment
 	@Override
 	public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
 		super.onViewCreated(view, savedInstanceState);
-		logger.debug("*** onViewCreated");
+		logger.debug("onViewCreated");
 
 		if (getActivity() != null && listView != null) {
 			// add text view if contact list is empty
@@ -1104,7 +1119,7 @@ public class ContactsSectionFragment
 	}
 
 	private void setupListeners() {
-		logger.debug("*** setup listeners");
+		logger.debug("setup listeners");
 
 		//set listeners
 		ListenerManager.contactListeners.add(this.contactListener);
@@ -1114,7 +1129,7 @@ public class ContactsSectionFragment
 	}
 
 	private void removeListeners() {
-		logger.debug("*** remove listeners");
+		logger.debug("remove listeners");
 
 		ListenerManager.contactListeners.remove(this.contactListener);
 		ListenerManager.contactSettingsListeners.remove(this.contactSettingsListener);

+ 13 - 26
app/src/main/java/ch/threema/app/fragments/MyIDFragment.java

@@ -31,13 +31,14 @@ import android.text.InputType;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.AdapterView;
 import android.widget.ArrayAdapter;
 import android.widget.ImageView;
 import android.widget.RelativeLayout;
 import android.widget.TextView;
 import android.widget.Toast;
 
+import com.google.android.material.textfield.MaterialAutoCompleteTextView;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -46,7 +47,6 @@ import java.util.Date;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
-import androidx.appcompat.widget.AppCompatSpinner;
 import androidx.core.widget.NestedScrollView;
 import androidx.fragment.app.DialogFragment;
 import androidx.fragment.app.FragmentManager;
@@ -165,9 +165,9 @@ public class MyIDFragment extends MainFragment
 						@Override
 						public void run() {
 							if (isAdded() && !isDetached() && fragmentView != null) {
-								AppCompatSpinner spinner = fragmentView.findViewById(R.id.picrelease_spinner);
+								MaterialAutoCompleteTextView spinner = fragmentView.findViewById(R.id.picrelease_spinner);
 								if (spinner != null) {
-									spinner.setSelection(preferenceService.getProfilePicRelease());
+									spinner.setText((CharSequence) spinner.getAdapter().getItem(preferenceService.getProfilePicRelease()), false);
 								}
 							}
 						}
@@ -262,25 +262,16 @@ public class MyIDFragment extends MainFragment
 				this.fragmentView.findViewById(R.id.profile_edit).setOnClickListener(this);
 			}
 
-			AppCompatSpinner spinner = fragmentView.findViewById(R.id.picrelease_spinner);
-			ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(getContext(), R.array.picrelease_choices, android.R.layout.simple_spinner_item);
-			adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+			MaterialAutoCompleteTextView spinner = fragmentView.findViewById(R.id.picrelease_spinner);
+			ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(getContext(), R.array.picrelease_choices, android.R.layout.simple_spinner_dropdown_item);
 			spinner.setAdapter(adapter);
-			spinner.setSelection(preferenceService.getProfilePicRelease());
-			spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
-				@Override
-				public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
-					int oldPosition = preferenceService.getProfilePicRelease();
-					preferenceService.setProfilePicRelease(position);
-					picReleaseConfImageView.setVisibility(position == PreferenceService.PROFILEPIC_RELEASE_SOME ? View.VISIBLE : View.GONE);
-					if (position == PreferenceService.PROFILEPIC_RELEASE_SOME && position != oldPosition) {
-						launchProfilePictureRecipientsSelector(view);
-					}
-				}
-
-				@Override
-				public void onNothingSelected(AdapterView<?> parent) {
-
+			spinner.setText(adapter.getItem(preferenceService.getProfilePicRelease()), false);
+			spinner.setOnItemClickListener((parent, view, position, id) -> {
+				int oldPosition = preferenceService.getProfilePicRelease();
+				preferenceService.setProfilePicRelease(position);
+				picReleaseConfImageView.setVisibility(position == PreferenceService.PROFILEPIC_RELEASE_SOME ? View.VISIBLE : View.GONE);
+				if (position == PreferenceService.PROFILEPIC_RELEASE_SOME && position != oldPosition) {
+					launchProfilePictureRecipientsSelector(view);
 				}
 			});
 
@@ -317,8 +308,6 @@ public class MyIDFragment extends MainFragment
 	}
 
 	private void updatePendingState(final View fragmentView, boolean force) {
-		logger.debug("*** updatePendingState");
-
 		if(!this.requiredInstances()) {
 			return;
 		}
@@ -345,8 +334,6 @@ public class MyIDFragment extends MainFragment
 	private boolean updatePendingStateTexts(View fragmentView) {
 		boolean pending = false;
 
-		logger.debug("*** updatePendingStateTexts");
-
 		if(!this.requiredInstances()) {
 			return false;
 		}

+ 5 - 0
app/src/main/java/ch/threema/app/fragments/UserMemberListFragment.java

@@ -95,6 +95,11 @@ public class UserMemberListFragment extends MemberListFragment {
 						public Boolean includeHidden() {
 							return false;
 						}
+
+						@Override
+						public Boolean onlyWithReceiptSettings() {
+							return false;
+						}
 					});
 				} else if (profilePics) {
 					contactModels = contactService.getCanReceiveProfilePics();

+ 5 - 0
app/src/main/java/ch/threema/app/fragments/WorkUserListFragment.java

@@ -146,6 +146,11 @@ public class WorkUserListFragment extends RecipientListFragment {
 					public Boolean includeHidden() {
 						return false;
 					}
+
+					@Override
+					public Boolean onlyWithReceiptSettings() {
+						return false;
+					}
 				}), new IPredicateNonNull<ContactModel>() {
 					@Override
 					public boolean apply(@NonNull ContactModel type) {

+ 5 - 0
app/src/main/java/ch/threema/app/fragments/WorkUserMemberListFragment.java

@@ -93,6 +93,11 @@ public class WorkUserMemberListFragment extends MemberListFragment {
 					public Boolean includeHidden() {
 						return false;
 					}
+
+					@Override
+					public Boolean onlyWithReceiptSettings() {
+						return false;
+					}
 				}), new IPredicateNonNull<ContactModel>() {
 					@Override
 					public boolean apply(@NonNull ContactModel type) {

+ 18 - 0
app/src/main/java/ch/threema/app/fragments/wizard/WizardFragment1.java

@@ -24,6 +24,7 @@ package ch.threema.app.fragments.wizard;
 import android.os.Bundle;
 import android.text.Editable;
 import android.text.TextWatcher;
+import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -34,6 +35,7 @@ import android.widget.TextView;
 import com.google.android.material.textfield.TextInputLayout;
 
 import ch.threema.app.R;
+import ch.threema.app.activities.wizard.WizardBaseActivity;
 import ch.threema.app.threemasafe.ThreemaSafeAdvancedDialog;
 import ch.threema.app.threemasafe.ThreemaSafeServerInfo;
 import ch.threema.app.utils.AppRestrictionUtil;
@@ -76,6 +78,22 @@ public class WizardFragment1 extends WizardFragment implements ThreemaSafeAdvanc
 
 		this.password1.addTextChangedListener(new PasswordWatcher());
 		this.password2.addTextChangedListener(new PasswordWatcher());
+		this.password2.setOnKeyListener(new View.OnKeyListener() {
+			@Override
+			public boolean onKey(View v, int keyCode, KeyEvent event) {
+				if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
+					if (password1.getText() != null && password2.getText() != null) {
+						if (getPasswordOK(password1.getText().toString(), password2.getText().toString())) {
+							if (getActivity() != null && isAdded()) {
+								((WizardBaseActivity) getActivity()).nextPage();
+							}
+							return true;
+						}
+					}
+				}
+				return false;
+			}
+		});
 
 		Button advancedOptions = rootView.findViewById(R.id.advanced_options);
 		advancedOptions.setVisibility(View.VISIBLE);

+ 23 - 5
app/src/main/java/ch/threema/app/fragments/wizard/WizardFragment2.java

@@ -25,12 +25,14 @@ import android.os.Bundle;
 import android.os.Handler;
 import android.text.Editable;
 import android.text.TextWatcher;
+import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.EditText;
 
 import ch.threema.app.R;
+import ch.threema.app.activities.wizard.WizardBaseActivity;
 import ch.threema.app.utils.EditTextUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.TestUtil;
@@ -72,6 +74,15 @@ public class WizardFragment2 extends WizardFragment {
 				}
 			});
 		}
+		this.nicknameText.setOnKeyListener((v, keyCode, event) -> {
+			if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
+				if (getActivity() != null && isAdded()) {
+					((WizardBaseActivity) getActivity()).nextPage();
+				}
+				return true;
+			}
+			return false;
+		});
 
 		return rootView;
 	}
@@ -93,11 +104,18 @@ public class WizardFragment2 extends WizardFragment {
 	@Override
 	public void onResume() {
 		super.onResume();
-		new Handler().postDelayed(() -> RuntimeUtil.runOnUiThread(this::initValues), 50);
-		if (this.nicknameText != null) {
-			this.nicknameText.requestFocus();
-			EditTextUtil.showSoftKeyboard(this.nicknameText);
-		}
+		new Handler().postDelayed(() -> {
+			RuntimeUtil.runOnUiThread(new Runnable() {
+				@Override
+				public void run() {
+					initValues();
+					if (nicknameText != null) {
+						nicknameText.requestFocus();
+						EditTextUtil.showSoftKeyboard(nicknameText);
+					}
+				}
+			});
+		}, 50);
 	}
 
 	@Override

+ 31 - 5
app/src/main/java/ch/threema/app/fragments/wizard/WizardFragment3.java

@@ -25,14 +25,17 @@ import android.app.Activity;
 import android.content.Context;
 import android.os.AsyncTask;
 import android.os.Bundle;
+import android.os.Handler;
 import android.text.Editable;
 import android.text.Selection;
 import android.text.TextUtils;
 import android.text.TextWatcher;
 import android.util.Patterns;
+import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
 import android.widget.AdapterView;
 import android.widget.BaseAdapter;
 import android.widget.EditText;
@@ -56,8 +59,10 @@ import java.util.Map;
 import java.util.Set;
 
 import ch.threema.app.R;
+import ch.threema.app.activities.wizard.WizardBaseActivity;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.EditTextUtil;
+import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.TestUtil;
 
 /**
@@ -189,6 +194,19 @@ public class WizardFragment3 extends WizardFragment {
 					}
 				}
 			});
+
+			if (!ConfigUtils.isWorkBuild()) {
+				this.phoneText.setImeOptions(EditorInfo.IME_ACTION_GO);
+				this.phoneText.setOnKeyListener((v, keyCode, event) -> {
+					if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
+						if (getActivity() != null && isAdded()) {
+							((WizardBaseActivity) getActivity()).nextPage();
+						}
+						return true;
+					}
+					return false;
+				});
+			}
 		}
 
 		TextView presetEmailText = rootView.findViewById(R.id.preset_email_text);
@@ -390,11 +408,19 @@ public class WizardFragment3 extends WizardFragment {
 	@Override
 	public void onResume() {
 		super.onResume();
-		initValues();
-		if (this.phoneText != null) {
-			this.phoneText.requestFocus();
-			EditTextUtil.showSoftKeyboard(this.phoneText);
-		}
+
+		new Handler().postDelayed(() -> {
+			RuntimeUtil.runOnUiThread(new Runnable() {
+				@Override
+				public void run() {
+					initValues();
+					if (phoneText != null) {
+						phoneText.requestFocus();
+						EditTextUtil.showSoftKeyboard(phoneText);
+					}
+				}
+			});
+		}, 50);
 	}
 
 	@Override

+ 1 - 2
app/src/main/java/ch/threema/app/managers/ServiceManager.java

@@ -167,7 +167,7 @@ public class ServiceManager {
 	private SystemScreenLockService systemScreenLockService;
 	private ShortcutService shortcutService;
 
-	private IdListService blackListService, excludedSyncIdentitiesService, profilePicRecipientsService;
+	private IdListService blackListService, excludedSyncIdentitiesService, profilePicRecipientsService, readReceiptsRecipientsService, isTypingRecipientsService;
 	private DeadlineListService mutedChatsListService, hiddenChatListService, mentionOnlyChatsListService;
 	private DistributionListService distributionListService;
 	private MessageProcessor messageProcessor;
@@ -382,7 +382,6 @@ public class ServiceManager {
 					this.getApiService(),
 					this.getWallpaperService(),
 					this.getLicenseService(),
-					this.getExcludedSyncIdentitiesService(),
 					this.getAPIConnector());
 		}
 

+ 4 - 4
app/src/main/java/ch/threema/app/mediaattacher/MediaAttachActivity.java

@@ -565,10 +565,10 @@ public class MediaAttachActivity extends MediaSelectionBaseActivity implements V
 			String mimeType = FileUtil.getMimeTypeFromUri(this, uri);
 			if (MimeUtil.isVideoFile(mimeType) || MimeUtil.isImageFile(mimeType)) {
 				try {
-					logger.info("Number of taken persistable uri permissions" + getContentResolver().getPersistedUriPermissions().size());
+					logger.info("Number of taken persistable uri permissions {}", getContentResolver().getPersistedUriPermissions().size());
 					getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
 				} catch (Exception e) {
-					logger.info("Unable to take persistable uri permission ", e);
+					logger.info(e.getMessage());
 					uri = FileUtil.getFileUri(uri);
 				}
 
@@ -603,10 +603,10 @@ public class MediaAttachActivity extends MediaSelectionBaseActivity implements V
 		for (Uri uri : list) {
 			try {
 				// log the number of permissions due to limit https://commonsware.com/blog/2020/06/13/count-your-saf-uri-permission-grants.html
-				logger.info("Number of taken persistable uri permissions" + getContentResolver().getPersistedUriPermissions().size());
+				logger.info("Number of taken persistable uri permissions {}", getContentResolver().getPersistedUriPermissions().size());
 				getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
 			} catch (Exception e) {
-				logger.info("Unable to take persistable uri permission ", e);
+				logger.info(e.getMessage());
 				uri = FileUtil.getFileUri(uri);
 			}
 

+ 5 - 1
app/src/main/java/ch/threema/app/mediaattacher/MediaSelectionBaseActivity.java

@@ -911,9 +911,13 @@ abstract public class MediaSelectionBaseActivity extends ThreemaActivity impleme
 		int firstVisible = gridLayoutManager.findFirstVisibleItemPosition();
 		if (firstVisible >= 0){
 			MediaAttachItem item = mediaAttachAdapter.getMediaItems().get(firstVisible);
-			dateView.post(() -> dateTextView.setText(LocaleUtil.formatDateRelative(MediaSelectionBaseActivity.this, item.getDateModified() * 1000)));
+			dateView.post(() -> {
+				dateTextView.setMaxLines(1);
+				dateTextView.setText(LocaleUtil.formatDateRelative(MediaSelectionBaseActivity.this, item.getDateModified() * 1000));
+			});
 		} else {
 			dateView.post(() -> {
+				dateTextView.setMaxLines(2);
 				dateTextView.setText(R.string.no_media_found_global);
 			});
 		}

+ 5 - 2
app/src/main/java/ch/threema/app/notifications/NotificationBuilderWrapper.java

@@ -21,6 +21,7 @@
 
 package ch.threema.app.notifications;
 
+import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
 import android.app.Notification;
 import android.app.NotificationChannel;
@@ -174,7 +175,7 @@ public class NotificationBuilderWrapper extends NotificationCompat.Builder {
 				if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(ringtone.getScheme())) {
 					// https://commonsware.com/blog/2016/09/07/notifications-sounds-android-7p0-aggravation.html
 					ThreemaApplication.getAppContext().grantUriPermission("com.android.systemui", ringtone, Intent.FLAG_GRANT_READ_URI_PERMISSION);
-				} else if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(ringtone.getScheme())) {
+				} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(ringtone.getScheme())) {
 					// content://settings/system/notification_sound
 					if (!ringtone.equals(Settings.System.DEFAULT_NOTIFICATION_URI)) {
 						// check if ringtone is still available
@@ -182,6 +183,8 @@ public class NotificationBuilderWrapper extends NotificationCompat.Builder {
 						} catch (Exception e) {
 							// cannot open ringtone - fallback to default ringtone
 							ringtone = Settings.System.DEFAULT_NOTIFICATION_URI;
+							logger.error("Unable to open ringtone. Falling back to default", e);
+							logger.info("Attempted to open {}", ringtone.toString());
 						}
 					}
 				}
@@ -382,7 +385,7 @@ public class NotificationBuilderWrapper extends NotificationCompat.Builder {
 				.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
 				.build();
 
-		NotificationChannel newNotificationChannel = new NotificationChannel(
+		@SuppressLint("WrongConstant") NotificationChannel newNotificationChannel = new NotificationChannel(
 				hash, hash.substring(0, 16),
 				notificationChannelSettings.getImportance());
 

+ 56 - 24
app/src/main/java/ch/threema/app/preference/SettingsPrivacyFragment.java

@@ -28,6 +28,7 @@ import android.os.Bundle;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.Toast;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -43,12 +44,14 @@ import ch.threema.app.ThreemaApplication;
 import ch.threema.app.activities.BlackListActivity;
 import ch.threema.app.activities.ExcludedSyncIdentitiesActivity;
 import ch.threema.app.dialogs.CancelableHorizontalProgressDialog;
+import ch.threema.app.dialogs.GenericAlertDialog;
 import ch.threema.app.dialogs.GenericProgressDialog;
 import ch.threema.app.exceptions.FileSystemNotPresentException;
 import ch.threema.app.listeners.SynchronizeContactsListener;
 import ch.threema.app.managers.ListenerManager;
 import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.routines.SynchronizeContactsRoutine;
+import ch.threema.app.services.ContactService;
 import ch.threema.app.services.SynchronizeContactsService;
 import ch.threema.app.utils.AppRestrictionUtil;
 import ch.threema.app.utils.ConfigUtils;
@@ -57,12 +60,13 @@ import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.SynchronizeContactsUtil;
 import ch.threema.localcrypto.MasterKeyLockedException;
 
-public class SettingsPrivacyFragment extends ThreemaPreferenceFragment implements CancelableHorizontalProgressDialog.ProgressDialogClickListener {
+public class SettingsPrivacyFragment extends ThreemaPreferenceFragment implements CancelableHorizontalProgressDialog.ProgressDialogClickListener, GenericAlertDialog.DialogClickListener {
 	private static final Logger logger = LoggerFactory.getLogger(SettingsPrivacyFragment.class);
 
 	private static final String DIALOG_TAG_VALIDATE = "vali";
 	private static final String DIALOG_TAG_SYNC_CONTACTS = "syncC";
 	private static final String DIALOG_TAG_DISABLE_SYNC = "dissync";
+	private static final String DIALOG_TAG_RESET_RECEIPTS = "rece";
 
 	private static final int PERMISSION_REQUEST_CONTACTS = 1;
 
@@ -123,11 +127,11 @@ public class SettingsPrivacyFragment extends ThreemaPreferenceFragment implement
 			logger.error("Exception", e);
 		}
 
-		this.disableScreenshot = (CheckBoxPreference) findPreference(getString(R.string.preferences__hide_screenshots));
+		this.disableScreenshot = findPreference(getString(R.string.preferences__hide_screenshots));
 		this.disableScreenshotChecked = this.disableScreenshot.isChecked();
 
-		this.contactSyncPreference = (TwoStatePreference) findPreference(getResources().getString(R.string.preferences__sync_contacts));
-		CheckBoxPreference blockUnknown = (CheckBoxPreference) findPreference(getString(R.string.preferences__block_unknown));
+		this.contactSyncPreference = findPreference(getResources().getString(R.string.preferences__sync_contacts));
+		CheckBoxPreference blockUnknown = findPreference(getString(R.string.preferences__block_unknown));
 
 		if (SynchronizeContactsUtil.isRestrictedProfile(getActivity())) {
 			// restricted android profile (e.g. guest user)
@@ -135,19 +139,17 @@ public class SettingsPrivacyFragment extends ThreemaPreferenceFragment implement
 			this.contactSyncPreference.setEnabled(false);
 			this.contactSyncPreference.setSelectable(false);
 		} else {
-			this.contactSyncPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
-				public boolean onPreferenceChange(Preference preference, Object newValue) {
-					boolean newCheckedValue = newValue.equals(true);
-					if (((TwoStatePreference) preference).isChecked() != newCheckedValue) {
-						if (newCheckedValue) {
-							enableSync();
-						} else {
-							disableSync();
-						}
+			this.contactSyncPreference.setOnPreferenceChangeListener((preference, newValue) -> {
+				boolean newCheckedValue = newValue.equals(true);
+				if (((TwoStatePreference) preference).isChecked() != newCheckedValue) {
+					if (newCheckedValue) {
+						enableSync();
+					} else {
+						disableSync();
 					}
-					//always return true, fix samsung preferences handler
-					return true;
 				}
+				//always return true, fix samsung preferences handler
+				return true;
 			});
 		}
 
@@ -175,24 +177,28 @@ public class SettingsPrivacyFragment extends ThreemaPreferenceFragment implement
 			this.disableScreenshot.setSelectable(false);
 		}
 
-		findPreference("pref_excluded_sync_identities").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
-			@Override
-			public boolean onPreferenceClick(Preference preference) {
-				startActivity(new Intent(getActivity(), ExcludedSyncIdentitiesActivity.class));
-				return false;
-			}
+		findPreference("pref_excluded_sync_identities").setOnPreferenceClickListener(preference -> {
+			startActivity(new Intent(getActivity(), ExcludedSyncIdentitiesActivity.class));
+			return false;
 		});
 
-		findPreference("pref_black_list").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+		findPreference("pref_black_list").setOnPreferenceClickListener(preference -> {
+			startActivity(new Intent(getActivity(), BlackListActivity.class));
+			return false;
+		});
+
+		findPreference("pref_reset_receipts").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
 			@Override
 			public boolean onPreferenceClick(Preference preference) {
-				startActivity(new Intent(getActivity(), BlackListActivity.class));
+				GenericAlertDialog dialog = GenericAlertDialog.newInstance(R.string.prefs_title_reset_receipts, getString(R.string.prefs_sum_reset_receipts) + "?", R.string.yes, R.string.no);
+				dialog.setTargetFragment(SettingsPrivacyFragment.this);
+				dialog.show(getFragmentManager(), DIALOG_TAG_RESET_RECEIPTS);
 				return false;
 			}
 		});
 
 		if (Build.VERSION.SDK_INT < 29) {
-			PreferenceCategory preferenceCategory = (PreferenceCategory) findPreference("pref_key_other");
+			PreferenceCategory preferenceCategory = findPreference("pref_key_other");
 			if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
 				preferenceCategory.removePreference(findPreference(getResources().getString(R.string.preferences__direct_share)));
 			}
@@ -307,6 +313,24 @@ public class SettingsPrivacyFragment extends ThreemaPreferenceFragment implement
 		return false;
 	}
 
+	private void resetReceipts() {
+		new Thread(() -> {
+			try {
+				ContactService contactService = ThreemaApplication.getServiceManager().getContactService();
+				contactService.resetReceiptsSettings();
+				RuntimeUtil.runOnUiThread(new Runnable() {
+					@Override
+					public void run() {
+						Toast.makeText(getContext(), R.string.reset_successful, Toast.LENGTH_SHORT).show();
+					}
+				});
+			} catch (Exception e) {
+				logger.error("ContactService not available", e);
+				Toast.makeText(getContext(), R.string.an_error_occurred, Toast.LENGTH_SHORT).show();
+			}
+		}, "ResetReceiptSettings").start();
+	}
+
 	@Override
 	public void onViewCreated(View view, Bundle savedInstanceState) {
 		preferenceFragmentCallbackInterface.setToolbarTitle(R.string.prefs_privacy);
@@ -338,4 +362,12 @@ public class SettingsPrivacyFragment extends ThreemaPreferenceFragment implement
 
 	@Override
 	public void onCancel(String tag, Object object) { }
+
+	@Override
+	public void onYes(String tag, Object data) {
+		resetReceipts();
+	}
+
+	@Override
+	public void onNo(String tag, Object data) {	}
 }

+ 4 - 0
app/src/main/java/ch/threema/app/processors/MessageProcessor.java

@@ -405,6 +405,10 @@ public class MessageProcessor implements MessageProcessorInterface {
 			logger.error("File not found", f);
 			return true;
 		}
+		catch (BadMessageException e) {
+			logger.warn("Bad message exception", e);
+			return e.shouldDrop();
+		}
 		catch (Exception e) {
 			logger.error("Unknown exception", e);
 			return false;

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

@@ -29,6 +29,8 @@ import android.database.Cursor;
 import android.os.Build;
 import android.provider.ContactsContract;
 
+import com.google.common.collect.ListMultimap;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -153,22 +155,23 @@ public class SynchronizeContactsRoutine implements Runnable {
 		this.running = true;
 
 		for(OnStarted s: this.onStarted) {
-			s.started(this.processingIdentities.size() == 0);
+			s.started(this.fullSync());
 		}
 
 		boolean success = false;
 
 		long deletedCount = 0;
-		long modifiedCount = 0;
+		long modifiedRawContactCount = 0;
+		long newRawContactCount = 0;
 		List<ContactModel> insertedContacts = new ArrayList<>();
 
 		try {
 			if(!this.preferenceService.isSyncContacts()) {
-				throw new ThreemaException("sync is disabled in preferences, not allowed to call synchronizecontacts routine");
+				throw new ThreemaException("Sync is disabled in preferences, not allowed to call synchronizecontacts routine");
 			}
 
 			if(this.deviceService != null && !this.deviceService.isOnline()) {
-				throw new ThreemaException("no connection");
+				throw new ThreemaException("No connection");
 			}
 
 			//read emails
@@ -182,39 +185,41 @@ public class SynchronizeContactsRoutine implements Runnable {
 			Map<String, APIConnector.MatchIdentityResult> foundIds = this.apiConnector.matchIdentities(
 					emails, phoneNumbers, this.localeService.getCountryIsoCode(), false, identityStore, matchTokenStore);
 
-			final List<String> preSynchronizedIdentities = new ArrayList<>();
-			HashMap<String, Long> existingRawContacts = new HashMap<>();
+			// remove own ID
+			if (userService != null) {
+				foundIds.remove(userService.getIdentity());
+			}
 
-			if(this.fullSync()) {
+			final List<String> preSynchronizedIdentities = new ArrayList<>();
+			if (this.fullSync()) {
 				List<String> synchronizedIdentities = this.contactService.getSynchronizedIdentities();
 				if (synchronizedIdentities != null) {
 					preSynchronizedIdentities.addAll(synchronizedIdentities);
 				}
+			}
 
-				existingRawContacts = AndroidContactUtil.getInstance().getAllThreemaRawContacts();
-				if (existingRawContacts != null) {
-					logger.debug("Number of existing raw contacts {}", existingRawContacts.size());
-				}
+			ListMultimap<String, AndroidContactUtil.RawContactInfo> existingRawContacts = AndroidContactUtil.getInstance().getAllThreemaRawContacts();
+			if (existingRawContacts == null) {
+				throw new ThreemaException("No permission or no account");
 			}
+			logger.info("Number of existing raw contacts {}", existingRawContacts.size());
 
 			//looping result and create/update contacts
+			logger.info("Number of IDs matching phone or email {}", foundIds.size());
+
 			ArrayList<ContentProviderOperation> contentProviderOperations = new ArrayList<>();
 			for (Map.Entry<String, APIConnector.MatchIdentityResult> id : foundIds.entrySet()) {
 				if(this.abort) {
 					//abort!
 					for(OnFinished f: this.onFinished) {
-						f.finished(false, modifiedCount, insertedContacts, deletedCount);
+						f.finished(false, modifiedRawContactCount, insertedContacts, deletedCount);
 					}
 					return;
 				}
 
-				// Do not add own ID as contact
-				if (TestUtil.compare(id.getKey(), this.userService.getIdentity())) {
-					continue;
-				}
-
 				// Do not sync contacts on exclude list
 				if(this.excludedSyncList != null && this.excludedSyncList.has(id.getKey())) {
+					logger.info("Identity {} is on exclude list", id.getKey());
 					continue;
 				}
 
@@ -222,8 +227,10 @@ public class SynchronizeContactsRoutine implements Runnable {
 					continue;
 				}
 
-				// remove if list contains this key
-				preSynchronizedIdentities.remove(id.getKey());
+				if (this.fullSync()) {
+					// remove if list contains this key
+					preSynchronizedIdentities.remove(id.getKey());
+				}
 
 				final ContactMatchKeyEmail matchKeyEmail = (ContactMatchKeyEmail)id.getValue().refObjectEmail;
 				final ContactMatchKeyPhone matchKeyPhone = (ContactMatchKeyPhone)id.getValue().refObjectMobileNo;
@@ -247,58 +254,63 @@ public class SynchronizeContactsRoutine implements Runnable {
 					contact.setVerificationLevel(VerificationLevel.SERVER_VERIFIED);
 					contact.setDateCreated(new Date());
 					insertedContacts.add(contact);
-				}
-				else {
-					modifiedCount++;
+					logger.info("Inserted new Threema contact {}", id.getKey());
 				}
 
-				if (contact.getVerificationLevel() == VerificationLevel.UNVERIFIED) {
-					contact.setVerificationLevel(VerificationLevel.SERVER_VERIFIED);
-				}
-
-				contact.setIsSynchronized(true);
-				contact.setIsHidden(false);
-				if (contactId > 0L) {
-					contact.setAndroidContactLookupKey(lookupKey + "/" + contactId); // It can optionally also have a "/" and last known contact ID appended after that. This "complete" format is an important optimization and is highly recommended.
-				}
-
-				if (fullSync()) {
-					Long parentContactId = existingRawContacts.get(contact.getIdentity());
-
-					if (parentContactId == null || parentContactId != contactId) {
-						if (contactId > 0L) {
-							// raw contact does not exist yet, create it
-							boolean supportsVoiceCalls = ContactUtil.canReceiveVoipMessages(contact, this.blackListIdentityService)
-								&& ConfigUtils.isCallsEnabled(context, preferenceService, licenseService);
-
-							// create a raw contact for our stuff and aggregate it
-							AndroidContactUtil.getInstance().createThreemaRawContact(
-								contentProviderOperations,
-								matchKeyEmail != null ?
-									matchKeyEmail.rawContactId :
-									matchKeyPhone.rawContactId,
-								contact,
-								supportsVoiceCalls);
-
-							// delete entry after processing - remaining ("stray") entries will be deleted from raw contacts after this run
-							existingRawContacts.remove(contact.getIdentity());
-						}
-					} else {
-						// all good - we can delete the hash
-						existingRawContacts.remove(contact.getIdentity());
-					}
-				}
+				contact.setAndroidContactLookupKey(lookupKey + "/" + contactId); // It can optionally also have a "/" and last known contact ID appended after that. This "complete" format is an important optimization and is highly recommended.
 
 				try {
+					boolean createNewRawContact = false;
+
 					AndroidContactUtil.getInstance().updateNameByAndroidContact(contact);
 					AndroidContactUtil.getInstance().updateAvatarByAndroidContact(contact);
 
-					// save the contact
-					this.contactService.save(contact);
+					contact.setIsHidden(false);
+					if (contact.getVerificationLevel() == VerificationLevel.UNVERIFIED) {
+						contact.setVerificationLevel(VerificationLevel.SERVER_VERIFIED);
+					}
+
+					List<AndroidContactUtil.RawContactInfo> rawContactInfos = existingRawContacts.get(contact.getIdentity());
+
+					if (rawContactInfos == null || rawContactInfos.size() == 0) {
+						// raw contact does not exist yet, create it
+						createNewRawContact = true;
+						newRawContactCount++;
+					} else {
+						// a raw contact exists - check if it points to the correct parent
+						createNewRawContact = true;
+						for (AndroidContactUtil.RawContactInfo rawContactInfo : rawContactInfos) {
+							if (rawContactInfo.contactId > 0L && rawContactInfo.contactId == contactId) {
+								// all good - no change necessary
+								createNewRawContact = false;
+								existingRawContacts.remove(contact.getIdentity(), rawContactInfo);
+								break;
+							}
+						}
+						if (createNewRawContact) {
+							modifiedRawContactCount++;
+						}
+					}
+
+					if (createNewRawContact) {
+						boolean supportsVoiceCalls = ContactUtil.canReceiveVoipMessages(contact, this.blackListIdentityService)
+							&& ConfigUtils.isCallsEnabled(context, preferenceService, licenseService);
+
+						// create a raw contact for our stuff and aggregate it
+						AndroidContactUtil.getInstance().createThreemaRawContact(
+							contentProviderOperations,
+							matchKeyEmail != null ?
+								matchKeyEmail.rawContactId :
+								matchKeyPhone.rawContactId,
+							contact,
+							supportsVoiceCalls);
+					}
 				} catch (ThreemaException e) {
-					existingRawContacts.put(contact.getIdentity(), 0L);
 					logger.error("Contact lookup Exception", e);
 				}
+
+				// save the contact
+				this.contactService.save(contact);
 			}
 
 			if (contentProviderOperations.size() > 0) {
@@ -312,37 +324,39 @@ public class SynchronizeContactsRoutine implements Runnable {
 				contentProviderOperations.clear();
 			}
 
-			// delete remaining / stray raw contacts
-			if (existingRawContacts.size() > 0) {
-				AndroidContactUtil.getInstance().deleteThreemaRawContacts(existingRawContacts);
-			}
+			if (this.fullSync()) {
+				// delete remaining / stray raw contacts
+				if (existingRawContacts.size() > 0) {
+					AndroidContactUtil.getInstance().deleteThreemaRawContacts(existingRawContacts);
+					logger.info("Deleted {} stray raw contacts", existingRawContacts.size());
+				}
 
-			if (preSynchronizedIdentities.size() > 0) {
-				logger.debug("Degrade contact(s). found {} synchronized contacts that are not synchronized" , preSynchronizedIdentities.size());
+				if (preSynchronizedIdentities.size() > 0) {
+					logger.info("Found {} synchronized contacts that are no longer synchronized", preSynchronizedIdentities.size());
 
-				List<ContactModel> contactModels = this.contactService.getByIdentities(preSynchronizedIdentities);
-				modifiedCount += this.contactService.save(
+					List<ContactModel> contactModels = this.contactService.getByIdentities(preSynchronizedIdentities);
+					this.contactService.save(
 						contactModels,
 						new ContactService.ContactProcessor() {
 							@Override
 							public boolean process(ContactModel contactModel) {
-								contactModel.setIsSynchronized(false);
+								contactModel.setAndroidContactLookupKey(null);
 								return true;
 							}
 						}
-				);
+					);
+				}
 			}
 			success = true;
 		} catch (final Exception x) {
-			success = false;
 			logger.error("Exception", x);
 			if (this.onStatusUpdate != null) {
 				this.onStatusUpdate.error(x);
 			}
 		} finally {
-			logger.debug("Finished [success=" + success + ", modified=" + modifiedCount + ", inserted =" + insertedContacts.size() + ", deleted =" + deletedCount + "]");
+			logger.info("Finished [success = {}, inserted = {}, deleted = {}, modifiedRawContacts = {}, newRawContacts = {}] fullSync = {}", success, insertedContacts.size(), deletedCount, modifiedRawContactCount, newRawContactCount, this.fullSync());
 			for (OnFinished f : this.onFinished) {
-				f.finished(success, modifiedCount, insertedContacts, deletedCount);
+				f.finished(success, modifiedRawContactCount, insertedContacts, deletedCount);
 			}
 			this.running = false;
 		}
@@ -396,7 +410,7 @@ public class SynchronizeContactsRoutine implements Runnable {
 					String lookupKey = phonesCursor.getString(lookupKeyColumnIndex);
 					String phoneNumber = phonesCursor.getString(phoneNumberIndex);
 
-					if (lookupKey != null && !TestUtil.empty(phoneNumber)) {
+					if (rawContactId > 0L && contactId > 0L && lookupKey != null && !TestUtil.empty(phoneNumber)) {
 						ContactMatchKeyPhone matchKey = new ContactMatchKeyPhone();
 						matchKey.contactId = contactId;
 						matchKey.lookupKey = lookupKey;
@@ -443,7 +457,7 @@ public class SynchronizeContactsRoutine implements Runnable {
 					String lookupKey = emailsCursor.getString(lookupKeyColumnIndex);
 					String email = emailsCursor.getString(emailIndex);
 
-					if (lookupKey != null && !TestUtil.empty(email)) {
+					if (rawContactId > 0L && contactId > 0L && lookupKey != null && !TestUtil.empty(email)) {
 						ContactMatchKeyEmail matchKey = new ContactMatchKeyEmail();
 						matchKey.contactId = contactId;
 						matchKey.lookupKey = lookupKey;

+ 10 - 2
app/src/main/java/ch/threema/app/services/ContactService.java

@@ -29,6 +29,7 @@ import java.util.List;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
 import ch.threema.app.exceptions.EntryAlreadyExistsException;
 import ch.threema.app.exceptions.InvalidEntryException;
 import ch.threema.app.exceptions.PolicyViolationException;
@@ -85,6 +86,11 @@ public interface ContactService extends AvatarService<ContactModel> {
 		 * include hidden contacts
 		 */
 		Boolean includeHidden();
+
+		/*
+		 * Limit to contacts with individual settings fro read receipts and typing indicators
+		 */
+		Boolean onlyWithReceiptSettings();
 	}
 
 	ContactModel getMe();
@@ -115,7 +121,6 @@ public interface ContactService extends AvatarService<ContactModel> {
 	int countIsWork();
 
 	List<ContactModel> getCanReceiveProfilePics();
-
 	List<String> getSynchronizedIdentities();
 
 	@Nullable
@@ -195,7 +200,7 @@ public interface ContactService extends AvatarService<ContactModel> {
 
 	boolean updateAllContactNamesAndAvatarsFromAndroidContacts();
 
-	void removeAllThreemaContactIds();
+	void removeAllSystemContactLinks();
 
 	boolean rebuildColors();
 
@@ -222,4 +227,7 @@ public interface ContactService extends AvatarService<ContactModel> {
 	String getAndroidContactLookupUriString(ContactModel contactModel);
 	@Nullable ContactModel addWorkContact(@NonNull WorkContact workContact, @Nullable List<ContactModel> existingWorkContacts);
 	void createWorkContact(@NonNull String identity);
+
+	@WorkerThread
+	boolean resetReceiptsSettings();
 }

+ 87 - 44
app/src/main/java/ch/threema/app/services/ContactServiceImpl.java

@@ -28,7 +28,6 @@ import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.Build;
 import android.provider.ContactsContract;
-import android.text.TextUtils;
 import android.text.format.DateUtils;
 
 import com.neilalexander.jnacl.NaCl;
@@ -59,6 +58,7 @@ import java.util.TimerTask;
 import androidx.annotation.ColorInt;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
 import androidx.core.content.ContextCompat;
 import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
 import ch.threema.app.R;
@@ -123,16 +123,16 @@ public class ContactServiceImpl implements ContactService {
 	private final MessageQueue messageQueue;
 	private final IdentityStore identityStore;
 	private final PreferenceService preferenceService;
-	private final IdListService excludeFromSyncListService;
 	private final Map<String, ContactModel> contactModelCache;
 	private final IdListService blackListIdentityService, profilePicRecipientsService;
-	private DeadlineListService mutedChatsListService, hiddenChatsListService;
-	private RingtoneService ringtoneService;
+	private final DeadlineListService mutedChatsListService;
+	private final DeadlineListService hiddenChatsListService;
+	private final RingtoneService ringtoneService;
 	private final FileService fileService;
 	private final ApiService apiService;
 	private final WallpaperService wallpaperService;
 	private final LicenseService licenseService;
-	private APIConnector apiConnector;
+	private final APIConnector apiConnector;
 	private final Timer typingTimer;
 	private final Map<String,TimerTask> typingTimerTasks;
 	private final VectorDrawableCompat contactDefaultAvatar;
@@ -158,7 +158,7 @@ public class ContactServiceImpl implements ContactService {
 		}
 	};
 
-	class ContactPhotoUploadResult {
+	static class ContactPhotoUploadResult {
 		public byte[] bitmapArray;
 		public byte[] blobId;
 		public byte[] encryptionKey;
@@ -185,7 +185,6 @@ public class ContactServiceImpl implements ContactService {
 			ApiService apiService,
 			WallpaperService wallpaperService,
 			LicenseService licenseService,
-			IdListService excludeFromSyncListService,
 			APIConnector apiConnector) {
 
 		this.context = context;
@@ -206,7 +205,6 @@ public class ContactServiceImpl implements ContactService {
 		this.apiService = apiService;
 		this.wallpaperService = wallpaperService;
 		this.licenseService = licenseService;
-		this.excludeFromSyncListService = excludeFromSyncListService;
 		this.apiConnector = apiConnector;
 		this.typingTimer = new Timer();
 		this.typingTimerTasks = new HashMap<>();
@@ -272,6 +270,11 @@ public class ContactServiceImpl implements ContactService {
 			public Boolean includeHidden() {
 				return includeHiddenContacts;
 			}
+
+			@Override
+			public Boolean onlyWithReceiptSettings() {
+				return false;
+			}
 		});
 	}
 
@@ -303,6 +306,10 @@ public class ContactServiceImpl implements ContactService {
 				placeholders.add(getMe().getIdentity());
 			}
 
+			if (filter.onlyWithReceiptSettings()) {
+				queryBuilder.appendWhere(ContactModel.COLUMN_TYPING_INDICATORS + " !=0 OR " + ContactModel.COLUMN_READ_RECEIPTS + " !=0");
+			}
+
 			result = contactModelFactory.convert
 					(
 							queryBuilder,
@@ -375,23 +382,8 @@ public class ContactServiceImpl implements ContactService {
 	}
 
 	private String getSortKey(ContactModel contactModel, boolean sortOrderFirstName) {
-		String key = contactModel.getIdentity();
+		String key = ContactUtil.getSafeNameString(contactModel, sortOrderFirstName);
 
-		if (sortOrderFirstName) {
-			if (!TextUtils.isEmpty(contactModel.getLastName())) {
-				key = contactModel.getLastName() + " " + key;
-			}
-			if (!TextUtils.isEmpty(contactModel.getFirstName())) {
-				key = contactModel.getFirstName() + " " + key;
-			}
-		} else {
-			if (!TextUtils.isEmpty(contactModel.getFirstName())) {
-				key = contactModel.getFirstName() + " " + key;
-			}
-			if (!TextUtils.isEmpty(contactModel.getLastName())) {
-				key = contactModel.getLastName() + " " + key;
-			}
-		}
 		if (contactModel.getIdentity().startsWith("*")) {
 			key = "\uFFFF" + key;
 		}
@@ -533,6 +525,11 @@ public class ContactServiceImpl implements ContactService {
 			public Boolean includeHidden() {
 				return false;
 			}
+
+			@Override
+			public Boolean onlyWithReceiptSettings() {
+				return false;
+			}
 		}), new IPredicateNonNull<ContactModel>() {
 
 			@Override
@@ -547,8 +544,8 @@ public class ContactServiceImpl implements ContactService {
 	public List<String> getSynchronizedIdentities() {
 		Cursor c = this.databaseServiceNew.getReadableDatabase().rawQuery("" +
 				"SELECT identity FROM contacts " +
-				"WHERE isSynchronized = ?",
-				new String[]{"1"});
+				"WHERE androidContactId IS NOT NULL AND androidContactId != ?",
+				new String[]{""});
 
 		if(c != null) {
 			List<String> identities = new ArrayList<>();
@@ -1044,19 +1041,18 @@ public class ContactServiceImpl implements ContactService {
 			return false;
 		}
 
-		List<ContactModel> androidContacts = this.getAll(true, true);
-		if(androidContacts != null) {
-			for(ContactModel c: androidContacts) {
-				if(!TestUtil.empty(c.getAndroidContactLookupKey())) {
+		List<ContactModel> contactModels = this.getAll(true, true);
+		if(contactModels != null) {
+			for(ContactModel contactModel: contactModels) {
+				if(!TestUtil.empty(contactModel.getAndroidContactLookupKey())) {
 					try {
-						if (AndroidContactUtil.getInstance().updateNameByAndroidContact(c)) {
-							AndroidContactUtil.getInstance().updateAvatarByAndroidContact(c);
-							this.save(c);
-							this.contactStore.reset(c);
-						}
+						AndroidContactUtil.getInstance().updateNameByAndroidContact(contactModel);
+						AndroidContactUtil.getInstance().updateAvatarByAndroidContact(contactModel);
 					} catch (ThreemaException e) {
-						logger.error("Exception", e);
+						contactModel.setAndroidContactLookupKey(null);
+						logger.error("Unable to update contact name", e);
 					}
+					this.save(contactModel);
 				}
 			}
 		}
@@ -1064,13 +1060,12 @@ public class ContactServiceImpl implements ContactService {
 	}
 
 	@Override
-	public void removeAllThreemaContactIds() {
+	public void removeAllSystemContactLinks() {
 		for(ContactModel c: this.find(null)) {
-			if (c.isSynchronized()) {
+			if (c.getAndroidContactLookupKey() != null) {
 				c.setAndroidContactLookupKey(null);
-				c.setIsSynchronized(false);
+				this.save(c);
 			}
-			this.save(c);
 		}
 	}
 
@@ -1080,9 +1075,9 @@ public class ContactServiceImpl implements ContactService {
 		if(models != null) {
 			int[] colors = ColorUtil.getInstance().generateGoogleColorPalette(models.size());
 			for(int n = 0; n < colors.length; n++) {
-				ContactModel m = models.get(n);
-				m.setColor(colors[n]);
-				this.save(m);
+				ContactModel c = models.get(n);
+				c.setColor(colors[n]);
+				this.save(c);
 			}
 			return true;
 		}
@@ -1301,12 +1296,14 @@ public class ContactServiceImpl implements ContactService {
 				throw new InvalidEntryException(R.string.connection_error);
 			}
 
-			newContact = this.getByIdentity(identity);
 
 		} catch (FileNotFoundException e) {
 			throw new InvalidEntryException(R.string.invalid_threema_id);
+		} catch (ThreemaException e) {
+			// contact already exists - shouldn't happen
 		}
 
+		newContact = this.getByIdentity(identity);
 
 		if (newContact == null) {
 			throw new InvalidEntryException(R.string.invalid_threema_id);
@@ -1440,4 +1437,50 @@ public class ContactServiceImpl implements ContactService {
 			}
 		}
 	}
+
+	@Override
+	@WorkerThread
+	public boolean resetReceiptsSettings() {
+		List<ContactModel> contactModels = find(new Filter() {
+			@Override
+			public ContactModel.State[] states() {
+				return new ContactModel.State[]{ContactModel.State.ACTIVE, ContactModel.State.INACTIVE};
+			}
+
+			@Override
+			public Integer requiredFeature() {
+				return null;
+			}
+
+			@Override
+			public Boolean fetchMissingFeatureLevel() {
+				return null;
+			}
+
+			@Override
+			public Boolean includeMyself() {
+				return false;
+			}
+
+			@Override
+			public Boolean includeHidden() {
+				return true;
+			}
+
+			@Override
+			public Boolean onlyWithReceiptSettings() {
+				return true;
+			}
+		});
+
+		if (contactModels.size() > 0) {
+			for (ContactModel contactModel : contactModels) {
+				contactModel.setTypingIndicators(ContactModel.DEFAULT);
+				contactModel.setReadReceipts(ContactModel.DEFAULT);
+				save(contactModel);
+			}
+			return true;
+		}
+		return false;
+	}
 }

+ 11 - 6
app/src/main/java/ch/threema/app/services/GroupApiServiceImpl.java

@@ -40,6 +40,7 @@ import ch.threema.client.GroupId;
 import ch.threema.client.MessageId;
 import ch.threema.client.MessageQueue;
 import ch.threema.client.Utils;
+import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.GroupModel;
 
 public class GroupApiServiceImpl implements GroupApiService {
@@ -119,13 +120,17 @@ public class GroupApiServiceImpl implements GroupApiService {
 					logger.error("Exception", e);
 					continue;
 				}
-				AbstractGroupMessage groupMessage = createApiMessage.create(messageId);
-				groupMessage.setGroupId(groupId);
-				groupMessage.setGroupCreator(groupCreatorId);
-				groupMessage.setFromIdentity(this.userService.getIdentity());
-				groupMessage.setToIdentity(identity);
 
-				pendingGroupMessages.add(groupMessage);
+				ContactModel contactModel = this.contactService.getByIdentity(identity);
+				if (contactModel != null && contactModel.getState() != ContactModel.State.INVALID) {
+					AbstractGroupMessage groupMessage = createApiMessage.create(messageId);
+					groupMessage.setGroupId(groupId);
+					groupMessage.setGroupCreator(groupCreatorId);
+					groupMessage.setFromIdentity(this.userService.getIdentity());
+					groupMessage.setToIdentity(identity);
+
+					pendingGroupMessages.add(groupMessage);
+				}
 			}
 		}
 

+ 8 - 8
app/src/main/java/ch/threema/app/services/GroupServiceImpl.java

@@ -511,14 +511,14 @@ public class GroupServiceImpl implements GroupService {
 			if(model != null) {
 				@GroupState int groupState = getGroupState(model);
 
-				this.removeMemberFromGroup(model, msg.getFromIdentity());
-
-				ListenerManager.groupListeners.handle(new ListenerManager.HandleListener<GroupListener>() {
-					@Override
-					public void handle(GroupListener listener) {
-						listener.onGroupStateChanged(model, groupState, getGroupState(model));
-					}
-				});
+				if (this.removeMemberFromGroup(model, msg.getFromIdentity())) {
+					ListenerManager.groupListeners.handle(new ListenerManager.HandleListener<GroupListener>() {
+						@Override
+						public void handle(GroupListener listener) {
+							listener.onGroupStateChanged(model, groupState, getGroupState(model));
+						}
+					});
+				}
 
 				return true;
 			}

+ 14 - 2
app/src/main/java/ch/threema/app/services/MessageServiceImpl.java

@@ -1081,8 +1081,20 @@ public class MessageServiceImpl implements MessageService {
 		boolean saved = false;
 
 		if (MessageUtil.canMarkAsRead(message)) {
-			boolean sendDeliveryReceipt = MessageUtil.canSendDeliveryReceipt(message)
-					&&  this.preferenceService.isReadReceipts();
+			ContactModel contactModel = this.contactService.getByIdentity(message.getIdentity());
+
+			boolean sendDeliveryReceipt = MessageUtil.canSendDeliveryReceipt(message);
+			if (sendDeliveryReceipt && contactModel != null) {
+				if (this.preferenceService.isReadReceipts()) {
+					if (contactModel.getReadReceipts() == ContactModel.DONT_SEND) {
+						sendDeliveryReceipt = false;
+					}
+				} else {
+					if (contactModel.getReadReceipts() != ContactModel.SEND) {
+						sendDeliveryReceipt = false;
+					}
+				}
+			}
 
 			//save is read
 			message.setRead(true);

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

@@ -1251,9 +1251,9 @@ public class NotificationServiceImpl implements NotificationService {
 		synchronized (this.conversationNotifications) {
 			if (!conversationNotifications.isEmpty()) {
 				for (ConversationNotification conversationNotification : conversationNotifications) {
-					this.conversationNotifications.remove(conversationNotification);
 					this.cancelAndDestroyConversationNotification(conversationNotification);
 				}
+				conversationNotifications.clear();
 				showDefaultPinLockedNewMessageNotification();
 			}
 		}

+ 2 - 2
app/src/main/java/ch/threema/app/services/SynchronizeContactsServiceImpl.java

@@ -260,7 +260,7 @@ public class SynchronizeContactsServiceImpl implements SynchronizeContactsServic
 			}
 
 			int numDeleted = AndroidContactUtil.getInstance().deleteAllThreemaRawContacts();
-			logger.debug("*** deleted {} raw contacts", numDeleted);
+			logger.debug("Deleted {} raw contacts", numDeleted);
 
 			if(!this.userService.removeAccount(new AccountManagerCallback<Boolean>() {
 				@Override
@@ -280,7 +280,7 @@ public class SynchronizeContactsServiceImpl implements SynchronizeContactsServic
 		}
 
 		if(contactService != null) {
-			contactService.removeAllThreemaContactIds();
+			contactService.removeAllSystemContactLinks();
 
 			// cleanup / degrade remaining identities that are still server verified
 			List<String> identities = contactService.getIdentitiesByVerificationLevel(VerificationLevel.SERVER_VERIFIED);

+ 25 - 1
app/src/main/java/ch/threema/app/services/UserServiceImpl.java

@@ -69,8 +69,10 @@ import ch.threema.client.ProtocolDefines;
 import ch.threema.client.ThreemaFeature;
 import ch.threema.client.TypingIndicatorMessage;
 import ch.threema.client.Utils;
+import ch.threema.storage.models.ContactModel;
 
 import static ch.threema.app.ThreemaApplication.PHONE_LINKED_PLACEHOLDER;
+import static ch.threema.app.ThreemaApplication.getServiceManager;
 
 /**
  * This service class handle all user actions (db/identity....)
@@ -477,7 +479,29 @@ public class UserServiceImpl implements UserService, CreateIdentityRequestDataIn
 
 	@Override
 	public boolean isTyping(String toIdentity, boolean isTyping) {
-		if (!preferenceService.isTypingIndicator()) {
+		boolean canSendIsTyping = this.preferenceService.isTypingIndicator();
+
+		ContactModel contactModel = null;
+		try {
+			if (getServiceManager() != null) {
+				contactModel = getServiceManager().getContactService().getByIdentity(toIdentity);
+			}
+		} catch (Exception e) {
+			logger.error("Can't get ContactService for isTyping", e);
+		}
+		if (contactModel != null) {
+			if (canSendIsTyping) {
+				if (contactModel.getTypingIndicators() == ContactModel.DONT_SEND) {
+					canSendIsTyping = false;
+				}
+			} else {
+				if (contactModel.getReadReceipts() == ContactModel.SEND) {
+					canSendIsTyping = true;
+				}
+			}
+		}
+
+		if (!canSendIsTyping) {
 			return false;
 		}
 

+ 5 - 0
app/src/main/java/ch/threema/app/services/systemupdate/SystemUpdateToVersion39.java

@@ -83,6 +83,11 @@ public class SystemUpdateToVersion39 extends  UpdateToVersion implements UpdateS
 				public Boolean includeHidden() {
 					return true;
 				}
+
+				@Override
+				public Boolean onlyWithReceiptSettings() {
+					return false;
+				}
 			});
 		} catch (MasterKeyLockedException | FileSystemNotPresentException e) {
 			logger.error("update script 39 failed", e);

+ 63 - 0
app/src/main/java/ch/threema/app/services/systemupdate/SystemUpdateToVersion66.java

@@ -0,0 +1,63 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2020-2021 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.app.services.systemupdate;
+
+import net.sqlcipher.database.SQLiteDatabase;
+
+import java.sql.SQLException;
+
+import ch.threema.app.services.UpdateSystemService;
+
+/**
+ * Add a messageFlags field
+ */
+public class SystemUpdateToVersion66 extends UpdateToVersion implements UpdateSystemService.SystemUpdate {
+
+	private final SQLiteDatabase sqLiteDatabase;
+
+
+	public SystemUpdateToVersion66(SQLiteDatabase sqLiteDatabase) {
+		this.sqLiteDatabase = sqLiteDatabase;
+	}
+
+	@Override
+	public boolean runDirectly() throws SQLException {
+		if(!this.fieldExist(this.sqLiteDatabase, "contacts", "readReceipts")) {
+			sqLiteDatabase.rawExecSQL("ALTER TABLE contacts ADD COLUMN readReceipts TINYINT DEFAULT 0");
+		}
+		if(!this.fieldExist(this.sqLiteDatabase, "contacts", "typingIndicators")) {
+			sqLiteDatabase.rawExecSQL("ALTER TABLE contacts ADD COLUMN typingIndicators TINYINT DEFAULT 0");
+		}
+		return true;
+	}
+
+
+	@Override
+	public boolean runASync() {
+		return true;
+	}
+
+	@Override
+	public String getText() {
+		return "version 66 (add readReceipts and typingIndicators)";
+	}
+}

+ 11 - 80
app/src/main/java/ch/threema/app/stores/ContactStore.java

@@ -21,22 +21,14 @@
 
 package ch.threema.app.stores;
 
-import android.os.NetworkOnMainThreadException;
-
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.FileNotFoundException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
 import java.util.Date;
-import java.util.HashMap;
 
-import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import ch.threema.app.collections.Functional;
-import ch.threema.app.collections.IPredicateNonNull;
+import androidx.annotation.WorkerThread;
 import ch.threema.app.listeners.ContactListener;
 import ch.threema.app.managers.ListenerManager;
 import ch.threema.app.services.IdListService;
@@ -49,7 +41,6 @@ import ch.threema.base.ThreemaException;
 import ch.threema.base.VerificationLevel;
 import ch.threema.client.APIConnector;
 import ch.threema.client.ContactStoreInterface;
-import ch.threema.client.ContactStoreObserver;
 import ch.threema.client.IdentityState;
 import ch.threema.storage.DatabaseServiceNew;
 import ch.threema.storage.factories.ContactModelFactory;
@@ -58,12 +49,11 @@ import ch.threema.storage.models.ContactModel;
 public class ContactStore implements ContactStoreInterface {
 	private static final Logger logger = LoggerFactory.getLogger(ContactStore.class);
 
-	private APIConnector apiConnector;
+	private final APIConnector apiConnector;
 	private final PreferenceService preferenceService;
-	private DatabaseServiceNew databaseServiceNew;
+	private final DatabaseServiceNew databaseServiceNew;
 	private final IdListService blackListService;
 	private final IdListService excludeListService;
-	private final HashMap<String, ContactModel> cache = new HashMap<>();
 
 	public ContactStore(APIConnector apiConnector,
 	                    PreferenceService preferenceService,
@@ -122,14 +112,16 @@ public class ContactStore implements ContactStoreInterface {
 	}
 
 	/**
-	 * Fetch a public key for a identity and save the contact
+	 * Fetch a public key for an identity, create a contact and save it
 	 *
-	 * @param identity
+	 * @param identity Identity to add a contact for
 	 * @throws ThreemaException if a contact with this identity already exists
-	 * @return
+	 *          FileNotFoundException if identity was not found on the server
+	 * @return public key of identity in case of success, null otherwise
 	 */
-	public byte[] fetchPublicKeyForIdentity(String identity) throws FileNotFoundException {
-
+	@WorkerThread
+	@Nullable
+	public byte[] fetchPublicKeyForIdentity(String identity) throws FileNotFoundException, ThreemaException {
 		APIConnector.FetchIdentityResult result = null;
 		try {
 			Contact contact = this.getContactForIdentity(identity);
@@ -142,8 +134,6 @@ public class ContactStore implements ContactStoreInterface {
 		}
 		catch (FileNotFoundException e) {
 			throw e;
-		} catch (NetworkOnMainThreadException e) {
-			throw e;
 		} catch (Exception e) {
 			//do nothing
 			return null;
@@ -189,30 +179,12 @@ public class ContactStore implements ContactStoreInterface {
 	 */
 	@Nullable
 	public ContactModel getContactModelForIdentity(String identity) {
-		if (!this.cache.containsKey(identity)) {
-			return this.databaseServiceNew.getContactModelFactory().getByIdentity(identity);
-		}
-
-		return this.cache.get(identity);
+		return this.databaseServiceNew.getContactModelFactory().getByIdentity(identity);
 	}
 
 	@Nullable
 	public ContactModel getContactModelForPublicKey(final byte[] publicKey) {
-		//check cache first
-		for(String identity: this.cache.keySet()) {
-			if (Arrays.equals(publicKey, this.cache.get(identity).getPublicKey())) {
-				return this.cache.get(identity);
-			}
-		}
-
 		return this.databaseServiceNew.getContactModelFactory().getByPublicKey(publicKey);
-		/*
-		return Functional.select(ContactModelFactory.getAll(this.databaseServiceNew)this.databaseService.getContactDao().queryForAll(), new IPredicateNonNull<ContactModel>() {
-			@Override
-			public boolean apply(ContactModel contactModel) {
-				return Arrays.equals(publicKey, contactModel.getPublicKey());
-			}
-		});*/
 	}
 
 	@Nullable
@@ -220,16 +192,6 @@ public class ContactStore implements ContactStoreInterface {
 		return this.databaseServiceNew.getContactModelFactory().getByLookupKey(lookupKey);
 	}
 
-	@Override
-	public Collection<Contact> getAllContacts() {
-		Collection<Contact> contacts = new ArrayList<>();
-
-		contacts.addAll(this.databaseServiceNew.getContactModelFactory().getAll());
-
-		return contacts;
-
-	}
-
 	@Override
 	public void addContact(Contact contact) {
 		ContactModel contactModel = (ContactModel)contact;
@@ -284,26 +246,9 @@ public class ContactStore implements ContactStoreInterface {
 
 	public void removeContact(final ContactModel contactModel) {
 		this.databaseServiceNew.getContactModelFactory().delete(contactModel);
-
-		synchronized (this.cache) {
-			this.cache.remove(contactModel.getIdentity());
-		}
-
 		fireOnRemovedContact(contactModel);
 	}
 
-	@Override
-	@Deprecated
-	public void addContactStoreObserver(ContactStoreObserver observer) {
-		//NOT IN USE
-	}
-
-	@Override
-	@Deprecated
-	public void removeContactStoreObserver(ContactStoreObserver observer) {
-		//NOT IN USE
-	}
-
 	private void fireOnNewContact(final ContactModel createdContactModel) {
 		ListenerManager.contactListeners.handle(new ListenerManager.HandleListener<ContactListener>() {
 			@Override
@@ -336,18 +281,4 @@ public class ContactStore implements ContactStoreInterface {
 			}
 		});
 	}
-
-	public void reset(final ContactModel contactModel) {
-		synchronized (this.cache) {
-			ContactModel cached = Functional.select(this.cache, new IPredicateNonNull<ContactModel>() {
-				@Override
-				public boolean apply(@NonNull ContactModel contact) {
-					return contact.getIdentity().equals(contactModel.getIdentity());
-				}
-			});
-			if(cached != null) {
-//				this.cache.put(contactModel.getIdentity(), contactModel);
-			}
-		}
-	}
 }

+ 6 - 0
app/src/main/java/ch/threema/app/threemasafe/ThreemaSafeServiceImpl.java

@@ -167,6 +167,8 @@ public class ThreemaSafeServiceImpl implements ThreemaSafeService {
 	private static final String TAG_SAFE_CONTACT_NICKNAME = "nickname";
 	private static final String TAG_SAFE_CONTACT_HIDDEN = "hidden";
 	private static final String TAG_SAFE_CONTACT_PRIVATE = "private";
+	private static final String TAG_SAFE_CONTACT_READ_RECEIPTS = "readReceipts";
+	private static final String TAG_SAFE_CONTACT_TYPING_INDICATORS = "typingIndicators";
 
 	private static final String TAG_SAFE_GROUPS = "groups";
 	private static final String TAG_SAFE_GROUP_ID = "id";
@@ -897,6 +899,8 @@ public class ThreemaSafeServiceImpl implements ThreemaSafeService {
 						contactModel.setDateCreated(new Date(contact.optLong(TAG_SAFE_CONTACT_CREATED_AT, System.currentTimeMillis())));
 						contactModel.setColor(ColorUtil.getInstance().getRecordColor((int) contactModelFactory.count()));
 						contactModel.setIsRestored(true);
+						contactModel.setTypingIndicators(contact.optInt(TAG_SAFE_CONTACT_TYPING_INDICATORS, ContactModel.DEFAULT));
+						contactModel.setReadReceipts(contact.optInt(TAG_SAFE_CONTACT_READ_RECEIPTS, ContactModel.DEFAULT));
 						contactModelFactory.createOrUpdate(contactModel);
 
 						if (contact.optBoolean(TAG_SAFE_CONTACT_PRIVATE, false)) {
@@ -1242,6 +1246,8 @@ public class ThreemaSafeServiceImpl implements ThreemaSafeService {
 		contact.put(TAG_SAFE_CONTACT_LAST_NAME, contactModel.getLastName());
 		contact.put(TAG_SAFE_CONTACT_NICKNAME, contactModel.getPublicNickName());
 		contact.put(TAG_SAFE_CONTACT_HIDDEN, contactModel.isHidden());
+		contact.put(TAG_SAFE_CONTACT_TYPING_INDICATORS, contactModel.getTypingIndicators());
+		contact.put(TAG_SAFE_CONTACT_READ_RECEIPTS, contactModel.getReadReceipts());
 		contact.put(TAG_SAFE_CONTACT_PRIVATE, hiddenChatsListService.has(contactService.getUniqueIdString(contactModel)));
 
 		return contact;

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

@@ -217,15 +217,16 @@ public class MentionSelectorPopup extends PopupWindow implements MentionSelector
 
 	private MentionSelectorAdapter updateList(boolean init) {
 		List<ContactModel> groupContacts = contactService.getByIdentities(groupService.getGroupIdentities(groupModel));
+		final boolean isSortingFirstName = preferenceService.isContactListSortingFirstName();
 
-		Collections.sort(groupContacts, (model1, model2) -> ContactUtil.getSafeNameString(model1, preferenceService).compareTo(
-			ContactUtil.getSafeNameString(model2, preferenceService)
+		Collections.sort(groupContacts, (model1, model2) -> ContactUtil.getSafeNameString(model1, isSortingFirstName).compareTo(
+			ContactUtil.getSafeNameString(model2, isSortingFirstName)
 		));
 
 		groupContacts.add(allContactModel);
 
 		if (!init && filterText.length() - filterStart > 0) {
-			groupContacts = Functional.filter(groupContacts, (IPredicateNonNull<ContactModel>) contactModel -> ContactUtil.getSafeNameString(contactModel, preferenceService).toLowerCase().contains(filterText.substring(filterStart).toLowerCase()));
+			groupContacts = Functional.filter(groupContacts, (IPredicateNonNull<ContactModel>) contactModel -> ContactUtil.getSafeNameString(contactModel, isSortingFirstName).toLowerCase().contains(filterText.substring(filterStart).toLowerCase()));
 		}
 
 		if (groupContacts.size() < 1) {

+ 73 - 48
app/src/main/java/ch/threema/app/utils/AndroidContactUtil.java

@@ -39,12 +39,14 @@ import android.os.Build;
 import android.provider.ContactsContract;
 import android.widget.Toast;
 
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
 import java.util.Date;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
@@ -99,6 +101,7 @@ public class AndroidContactUtil {
 	};
 
 	private static final String[] RAW_CONTACT_PROJECTION = new String[] {
+		ContactsContract.RawContacts._ID,
 		ContactsContract.RawContacts.CONTACT_ID,
 		ContactsContract.RawContacts.SYNC1,
 	};
@@ -122,19 +125,29 @@ public class AndroidContactUtil {
 		return userService.getAccount();
 	}
 
+	public static class RawContactInfo {
+		public final long contactId;
+		public final long rawContactId;
+
+		RawContactInfo(long contactId, long rawContactId) {
+			this.contactId = contactId;
+			this.rawContactId = rawContactId;
+		}
+	}
+
 	private static class ContactName {
 		final String firstName;
 		final String lastName;
 
-		public ContactName(String firstName, String lastName) {
-			this.firstName = firstName;
-			this.lastName = lastName;
+		public ContactName(@Nullable String firstName, @Nullable String lastName) {
+			this.firstName = firstName != null ? firstName.trim() : firstName;
+			this.lastName = lastName != null ? lastName.trim() : lastName;
 		}
 	}
 
 	/**
 	 * Return a valid uri to the given contact that can be used to build an intent for the contact app
-	 * It is safe to call this method if permission to access contacts is not granted
+	 * It is safe to call this method if permission to access contacts is not granted - null will be returned in that case
 	 *
 	 * @param contactModel ContactModel for which to get the Android contact URI
 	 * @return a valid uri pointing to the android contact or null if permission was not granted, no android contact is linked or android contact could not be looked up
@@ -175,54 +188,50 @@ public class AndroidContactUtil {
 	 * @param contactModel ContactModel
 	 * @return true if setting or deleting the avatar was successful, false otherwise
 	 */
-	public boolean updateAvatarByAndroidContact(ContactModel contactModel) {
+	public boolean updateAvatarByAndroidContact(@NonNull ContactModel contactModel) {
 		if (fileService == null) {
 			logger.info("FileService not available");
 			return false;
 		}
 
 		String androidContactId = contactModel.getAndroidContactLookupKey();
-
-		if(TestUtil.empty(androidContactId)) {
+		if (TestUtil.empty(androidContactId)) {
 			return false;
 		}
 
+		// contactUri will be null if permission is not granted
 		Uri contactUri = getAndroidContactUri(contactModel);
-		if (contactUri == null) {
-			return false;
-		}
-
-		Bitmap bitmap = null;
-		if (ConfigUtils.isPermissionGranted(ThreemaApplication.getAppContext(), Manifest.permission.READ_CONTACTS)) {
-			bitmap = AvatarConverterUtil.convert(ThreemaApplication.getAppContext(), contactUri);
-		}
+		if (contactUri != null) {
+			Bitmap bitmap = AvatarConverterUtil.convert(ThreemaApplication.getAppContext(), contactUri);
 
-		if (bitmap != null) {
-			try {
-				fileService.writeAndroidContactAvatar(contactModel, BitmapUtil.bitmapToByteArray(bitmap, Bitmap.CompressFormat.PNG, 100));
-				contactModel.setAvatarExpires(new Date(System.currentTimeMillis() + DEFAULT_ANDROID_CONTACT_AVATAR_EXPIRY));
-				return true;
-			} catch (Exception e) {
-				logger.error("Exception", e);
-			}
-		} else {
-			// delete old avatar
-			boolean success = fileService.removeAndroidContactAvatar(contactModel);
-			if (success) {
-				contactModel.setAvatarExpires(new Date(System.currentTimeMillis() + DEFAULT_ANDROID_CONTACT_AVATAR_EXPIRY));
-				return true;
+			if (bitmap != null) {
+				try {
+					fileService.writeAndroidContactAvatar(contactModel, BitmapUtil.bitmapToByteArray(bitmap, Bitmap.CompressFormat.PNG, 100));
+					contactModel.setAvatarExpires(new Date(System.currentTimeMillis() + DEFAULT_ANDROID_CONTACT_AVATAR_EXPIRY));
+					return true;
+				} catch (Exception e) {
+					logger.error("Exception", e);
+				}
+			} else {
+				// delete old avatar
+				boolean success = fileService.removeAndroidContactAvatar(contactModel);
+				if (success) {
+					contactModel.setAvatarExpires(new Date(System.currentTimeMillis() + DEFAULT_ANDROID_CONTACT_AVATAR_EXPIRY));
+					return true;
+				}
 			}
 		}
 
+		logger.info("Unable to get avatar for {} lookupKey = {} contactUri = {}", contactModel.getIdentity(), contactModel.getAndroidContactLookupKey(), contactUri);
 		return false;
 	}
 
 	/**
 	 * Update the name of this contact according to the name of the Android contact
-	 * Note that the ContactModel needs to be saved to the ContactStore to apply the changes
+	 * Note that the ContactModel needs to be saved to the ContactStore to apply the changes!
 	 *
 	 * @param contactModel ContactModel
-	 * @return true if setting the name was successful, false otherwise
+	 * @return true if the name has changed, false otherwise
 	 */
 	@RequiresPermission(Manifest.permission.READ_CONTACTS)
 	public boolean updateNameByAndroidContact(@NonNull ContactModel contactModel) throws ThreemaException {
@@ -231,6 +240,9 @@ public class AndroidContactUtil {
 			ContactName contactName = this.getContactName(namedContactUri);
 
 			if (contactName == null) {
+				logger.info("Unable to get contact name for {} lookupKey = {} namedUri = {}", contactModel.getIdentity(), contactModel.getAndroidContactLookupKey(), namedContactUri);
+				// remove contact link to unresolvable contact
+				contactModel.setAndroidContactLookupKey(null);
 				throw new ThreemaException("Unable to get contact name");
 			}
 
@@ -240,6 +252,10 @@ public class AndroidContactUtil {
 				contactModel.setLastName(contactName.lastName);
 				return true;
 			}
+		} else {
+			if (contactModel != null) {
+				logger.info("Unable to get android contact uri for {} lookupkey = {}", contactModel.getIdentity(), contactModel.getAndroidContactLookupKey());
+			}
 		}
 		return false;
 	}
@@ -291,11 +307,12 @@ public class AndroidContactUtil {
 						}
 					} else {
 						// no contact name found
+						logger.info("No contact name found for contact ID {} uri = {}", contactId, contactUri.toString());
 						return null;
 					}
 				}
 			} else {
-				logger.debug("Contact not found: {}", contactUri.toString());
+				logger.info("Contact not found: {}", contactUri.toString());
 			}
 		} catch (PatternSyntaxException e) {
 			logger.error("Exception", e);
@@ -409,6 +426,10 @@ public class AndroidContactUtil {
 			return;
 		}
 
+		if (systemRawContactId == 0L) {
+			return;
+		}
+
 		int backReference = contentProviderOperations.size();
 		logger.debug("Adding contact: " + identity);
 
@@ -448,10 +469,12 @@ public class AndroidContactUtil {
 			builder.withValueBackReference(ContactsContract.AggregationExceptions.RAW_CONTACT_ID2, backReference);
 			builder.withValue(ContactsContract.AggregationExceptions.TYPE, ContactsContract.AggregationExceptions.TYPE_KEEP_TOGETHER);
 			contentProviderOperations.add(builder.build());
+
+		logger.info("Create a raw contact for ID {} and aggregate it with system raw contact {}", identity, systemRawContactId);
 	}
 
 	/**
-	 * Delete the raw contact where the given identity matches the entry in the contact's SYNC1 column
+	 * Delete all raw contacts where the given identity matches the entry in the contact's SYNC1 column
 	 * It's safe to call this method without contacts permission
 	 *
 	 * @param contactModel ContactModel whose raw contact we want to be deleted
@@ -485,10 +508,10 @@ public class AndroidContactUtil {
 	/**
 	 * Delete all raw contacts specified in rawContacts Map
 	 *
-	 * @param rawContacts HashMap of the rawContacts to delete. The key of the map entry contains the identity
+	 * @param rawContacts Map of the rawContacts to delete. The key of the map entry contains the identity
 	 * @return Number of raw contacts that were supposed to be deleted. Does not necessarily represent the real number of deleted raw contacts.
 	 */
-	public int deleteThreemaRawContacts(@NonNull HashMap<String, Long> rawContacts) {
+	public int deleteThreemaRawContacts(@NonNull ListMultimap<String, RawContactInfo> rawContacts) {
 		if (!ConfigUtils.isPermissionGranted(ThreemaApplication.getAppContext(), Manifest.permission.WRITE_CONTACTS)) {
 			return 0;
 		}
@@ -504,16 +527,17 @@ public class AndroidContactUtil {
 
 		ArrayList<ContentProviderOperation> contentProviderOperations = new ArrayList<>();
 
-		for (Map.Entry<String, Long> rawContact : rawContacts.entrySet()) {
-			if (!TestUtil.empty(rawContact.getKey())) {
+		for (Map.Entry<String, RawContactInfo> rawContact : rawContacts.entries()) {
+			if (!TestUtil.empty(rawContact.getKey()) && rawContact.getValue().rawContactId != 0L) {
 				ContentProviderOperation.Builder builder = ContentProviderOperation.newDelete(
 					ContactsContract.RawContacts.CONTENT_URI
 						.buildUpon()
 						.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
 						.appendQueryParameter(ContactsContract.RawContacts.SYNC1, rawContact.getKey())
 						.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, account.name)
-						.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type).build()
-				);
+						.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type).build())
+						.withSelection(ContactsContract.RawContacts._ID+" = ?", new String[] {String.valueOf(rawContact.getValue().rawContactId)});
+
 				contentProviderOperations.add(builder.build());
 			}
 		}
@@ -543,7 +567,7 @@ public class AndroidContactUtil {
 	 */
 	public int deleteAllThreemaRawContacts() {
 		if (!ConfigUtils.isPermissionGranted(ThreemaApplication.getAppContext(), Manifest.permission.WRITE_CONTACTS)) {
-			return  0;
+			return 0;
 		}
 
 		Account account = this.getAccount();
@@ -568,10 +592,10 @@ public class AndroidContactUtil {
 	/**
 	 * Get a list of all Threema raw contacts from the contact database. This may include "stray" contacts.
 	 *
-	 * @return HashMap containing identity as key and android contact id as value
+	 * @return List containing pairs of identity and android contact id, null if permissions have not been granted
 	 */
 	@Nullable
-	public HashMap<String, Long> getAllThreemaRawContacts() {
+	public ListMultimap<String, RawContactInfo> getAllThreemaRawContacts() {
 		if (!ConfigUtils.isPermissionGranted(ThreemaApplication.getAppContext(), Manifest.permission.WRITE_CONTACTS)) {
 			return null;
 		}
@@ -586,15 +610,16 @@ public class AndroidContactUtil {
 			.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, account.name)
 			.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type).build();
 
-		HashMap<String, Long> rawContacts = new HashMap<>();
+		ListMultimap<String, RawContactInfo> rawContacts = ArrayListMultimap.create();
 		Cursor cursor = null;
 		try {
 			cursor = contentResolver.query(rawContactUri, RAW_CONTACT_PROJECTION, null, null, null);
 			if (cursor != null){
 				while (cursor.moveToNext()) {
-					Long contactId = cursor.getLong(0);
-					String identity = cursor.getString(1);
-					rawContacts.put(identity, contactId);
+					long rawContactId = cursor.getLong(0);
+					long contactId = cursor.getLong(1);
+					String identity = cursor.getString(2);
+					rawContacts.put(identity, new RawContactInfo(contactId, rawContactId));
 				}
 			}
 		} catch (Exception e) {
@@ -655,7 +680,7 @@ public class AndroidContactUtil {
 			return null;
 		}
 
-		if (!contactModel.isSynchronized() || contactModel.getAndroidContactLookupKey() == null) {
+		if (contactModel.getAndroidContactLookupKey() == null) {
 			return null;
 		}
 

+ 26 - 32
app/src/main/java/ch/threema/app/utils/ContactUtil.java

@@ -26,6 +26,7 @@ import android.content.Intent;
 import android.database.Cursor;
 import android.graphics.drawable.Drawable;
 import android.provider.ContactsContract;
+import android.text.TextUtils;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -34,6 +35,7 @@ import java.util.Date;
 
 import androidx.annotation.DrawableRes;
 import androidx.appcompat.content.res.AppCompatResources;
+import androidx.core.util.Pair;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.services.FileService;
@@ -79,15 +81,6 @@ public class ContactUtil {
 		return canHaveCustomAvatar(contactModel)
 				&& !(preferenceService.getProfilePicReceive() && fileService.hasContactPhotoFile(contactModel));
 	}
-	/**
-	 * check if this contact was added during a synchronization run.
-	 * note that the contact may no longer be linked to a system contact
-	 * @param contact
-	 * @return
-	 */
-	public static boolean isSynchronized(ContactModel contact) {
-		return contact != null && contact.isSynchronized();
-	}
 
 	/**
 	 * return true on channel-type contact (i.e. gateway, threema broadcast)
@@ -176,32 +169,33 @@ public class ContactUtil {
 	 * returns a representation of the contact's name according to sort settings,
 	 * suitable for comparing
 	 */
-	public static String getSafeNameString(ContactModel c, PreferenceService preferenceService) {
-		if (preferenceService.isContactListSortingFirstName()) {
-			return (c.getFirstName() != null ? c.getFirstName() : "") +
-					(c.getLastName() != null ? c.getLastName() : "") +
-					(c.getPublicNickName() != null ? c.getPublicNickName() : "") +
-					c.getIdentity();
-		}
-		else {
-			return (c.getLastName() != null ? c.getLastName() : "") +
-					(c.getFirstName() != null ? c.getFirstName() : "") +
-					(c.getPublicNickName() != null ? c.getPublicNickName() : "") +
-					c.getIdentity();
+	public static String getSafeNameString(ContactModel contactModel, boolean sortOrderFirstName) {
+		String key = contactModel.getIdentity();
+		String firstName = contactModel.getFirstName();
+		String lastName = contactModel.getLastName();
+
+		if (TestUtil.empty(firstName) && TestUtil.empty(lastName) && !TestUtil.empty(contactModel.getPublicNickName())) {
+			Pair<String, String> namePair = NameUtil.getFirstLastNameFromDisplayName(contactModel.getPublicNickName().trim());
+			firstName = namePair.first;
+			lastName = namePair.second;
 		}
-	}
 
-	public static String getSafeNameStringNoNickname(ContactModel c, PreferenceService preferenceService) {
-		if (preferenceService.isContactListSortingFirstName()) {
-			return (c.getFirstName() != null ? c.getFirstName() : "") +
-					(c.getLastName() != null ? c.getLastName() : "") +
-					c.getIdentity();
-		}
-		else {
-			return (c.getLastName() != null ? c.getLastName() : "") +
-					(c.getFirstName() != null ? c.getFirstName() : "") +
-					c.getIdentity();
+		if (sortOrderFirstName) {
+			if (!TextUtils.isEmpty(lastName)) {
+				key = lastName + key;
+			}
+			if (!TextUtils.isEmpty(firstName)) {
+				key = firstName + key;
+			}
+		} else {
+			if (!TextUtils.isEmpty(firstName)) {
+				key = firstName + key;
+			}
+			if (!TextUtils.isEmpty(lastName)) {
+				key = lastName + key;
+			}
 		}
+		return key;
 	}
 
 	public static @DrawableRes int getVerificationResource(ContactModel contactModel) {

+ 8 - 0
app/src/main/java/ch/threema/app/utils/MediaPlayerStateWrapper.java

@@ -304,6 +304,14 @@ public class MediaPlayerStateWrapper {
 		mediaPlayer.setScreenOnWhilePlaying(screenOn);
 	}
 
+	public void setOnPreparedListener(MediaPlayer.OnPreparedListener onPreparedListener) {
+		mediaPlayer.setOnPreparedListener(onPreparedListener);
+	}
+
+	public void setOnCompletionListener(MediaPlayer.OnCompletionListener onCompletionListener) {
+		mediaPlayer.setOnCompletionListener(onCompletionListener);
+	}
+
 	/**
 	 * Set playback parameters of MediaPlayer instance
 	 * @param playbackParams PlaybackParams to set

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

@@ -320,12 +320,12 @@ public class QuoteUtil {
 			switch (messageModel.getType()) {
 				case IMAGE:
 				case FILE:
-				case TEXT:
 				case VIDEO:
-				case BALLOT:
 				case VOICEMESSAGE:
+				case TEXT:
+				case BALLOT:
 				case LOCATION:
-					return true;
+					return messageModel.getApiMessageId() != null;
 				default:
 					return false;
 			}

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

@@ -47,6 +47,7 @@ public class SynchronizeContactsUtil {
 	}
 
 	public static void startDirectly(String forIdentity) {
+		logger.info("Starting single contact sync for identity {}", forIdentity);
 		SynchronizeContactsRoutine routine = getSynchronizeContactsRoutine();
 		if(routine != null) {
 			routine.addProcessIdentity(forIdentity);

+ 27 - 11
app/src/main/java/ch/threema/app/voicemessage/VoiceRecorderActivity.java

@@ -67,6 +67,7 @@ import ch.threema.app.ui.DebouncedOnClickListener;
 import ch.threema.app.ui.MediaItem;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.IntentDataUtil;
+import ch.threema.app.utils.MediaPlayerStateWrapper;
 import ch.threema.app.utils.MimeUtil;
 
 public class VoiceRecorderActivity extends AppCompatActivity implements View.OnClickListener, AudioRecorder.OnStopListener, AudioManager.OnAudioFocusChangeListener, GenericAlertDialog.DialogClickListener, SensorListener {
@@ -92,7 +93,7 @@ public class VoiceRecorderActivity extends AppCompatActivity implements View.OnC
 	}
 
 	private MediaRecorder mediaRecorder;
-	private MediaPlayer mediaPlayer;
+	private MediaPlayerStateWrapper mediaPlayer;
 	private MediaState status = MediaState.STATE_NONE;
 	private TextView timerTextView;
 	private ImageView playButton;
@@ -580,21 +581,36 @@ public class VoiceRecorderActivity extends AppCompatActivity implements View.OnC
 	 * @return Duration in ms or 0 if the media player was unable to open this file
 	 */
 	private int getDurationFromFile() {
-		MediaPlayer mediaPlayer = MediaPlayer.create(this, uri);
-		if (mediaPlayer != null) {
-			int duration = mediaPlayer.getDuration();
-			mediaPlayer.release();
+		MediaPlayer durationCheckMediaPlayer = MediaPlayer.create(this, uri);
+		if (durationCheckMediaPlayer != null) {
+			int duration = durationCheckMediaPlayer.getDuration();
+			durationCheckMediaPlayer.release();
 			return duration;
 		}
 		return 0;
 	}
 
 	private void returnData() {
-		MediaItem mediaItem = new MediaItem(uri, MimeUtil.MIME_TYPE_AUDIO_AAC, null);
-		mediaItem.setDurationMs(getDurationFromFile());
-		messageService.sendMediaAsync(Collections.singletonList(mediaItem), Collections.singletonList(messageReceiver));
+		releaseMediaRecorder();
 
-		this.finish();
+		if (this.mediaPlayer != null) {
+			this.mediaPlayer.release();
+			this.mediaPlayer = null;
+		}
+
+		long fileduration = (long) getDurationFromFile();
+		if (fileduration > 0) {
+			if (fileduration < DateUtils.SECOND_IN_MILLIS) {
+				fileduration = DateUtils.SECOND_IN_MILLIS;
+			}
+			MediaItem mediaItem = new MediaItem(uri, MimeUtil.MIME_TYPE_AUDIO_AAC, null);
+			mediaItem.setDurationMs(fileduration);
+			messageService.sendMediaAsync(Collections.singletonList(mediaItem), Collections.singletonList(messageReceiver));
+			this.finish();
+		}
+		else {
+			Toast.makeText(this, R.string.unable_to_determine_recording_length, Toast.LENGTH_LONG).show();
+		}
 	}
 
 	@Override
@@ -692,7 +708,7 @@ public class VoiceRecorderActivity extends AppCompatActivity implements View.OnC
 		}
 	}
 
-	private void stopAndReleaseMediaPlayer(MediaPlayer mp) {
+	private void stopAndReleaseMediaPlayer(MediaPlayerStateWrapper mp) {
 		if (mp != null) {
 			stopTimer();
 			stopUpdateSeekbar();
@@ -713,7 +729,7 @@ public class VoiceRecorderActivity extends AppCompatActivity implements View.OnC
 				stopAndReleaseMediaPlayer(mediaPlayer);
 			}
 
-			mediaPlayer = new MediaPlayer();
+			mediaPlayer = new MediaPlayerStateWrapper();
 			if (scoAudioState == AudioManager.SCO_AUDIO_STATE_CONNECTED) {
 				mediaPlayer.setAudioStreamType(AudioManager.STREAM_VOICE_CALL);
 			} else {

+ 4 - 3
app/src/main/java/ch/threema/app/voip/PeerConnectionClient.java

@@ -1035,9 +1035,10 @@ public class PeerConnectionClient {
 		executor.execute(() -> {
 			enableLocalAudioTrack = enable;
 			if (localAudioTrack != null) {
-				if (localAudioTrack.enabled() != enableLocalAudioTrack) {
-					localAudioTrack.setEnabled(enableLocalAudioTrack);
-					this.sendSignalingMessage(CaptureState.microphone(enableLocalAudioTrack));
+				if (localAudioTrack.enabled() != enable) {
+					logger.info("Changing MICROPHONE capturing state to {}", (enable ? "ON" : "OFF"));
+					localAudioTrack.setEnabled(enable);
+					this.sendSignalingMessage(CaptureState.microphone(enable));
 				}
 			}
 		});

+ 16 - 3
app/src/main/java/ch/threema/app/voip/activities/CallActivity.java

@@ -51,6 +51,7 @@ import android.renderscript.RenderScript;
 import android.renderscript.ScriptIntrinsicBlur;
 import android.util.Rational;
 import android.view.GestureDetector;
+import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
@@ -294,8 +295,8 @@ public class CallActivity extends ThreemaActivity implements
 	 * The result of a permission request.
 	 */
 	private static class PermissionRequestResult {
-		private boolean _granted;
-		private boolean _wasAlreadyGranted;
+		private final boolean _granted;
+		private final boolean _wasAlreadyGranted;
 
 		public PermissionRequestResult(boolean granted, boolean wasAlreadyGranted) {
 			this._granted = granted;
@@ -624,7 +625,7 @@ public class CallActivity extends ThreemaActivity implements
 	};
 
 	//endregion
-	private VoipAudioManagerListener audioManagerListener = new VoipAudioManagerListener() {
+	private final VoipAudioManagerListener audioManagerListener = new VoipAudioManagerListener() {
 		@Override
 		public void onAudioDeviceChanged(@Nullable VoipAudioManager.AudioDevice selectedAudioDevice, @NonNull HashSet<VoipAudioManager.AudioDevice> availableAudioDevices) {
 			if (selectedAudioDevice != null) {
@@ -1003,6 +1004,18 @@ public class CallActivity extends ThreemaActivity implements
 		}
 	}
 
+	@Override
+	public boolean onKeyDown(int keyCode, KeyEvent event) {
+		if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN){
+			// mute notification
+			if (voipStateService != null) {
+				voipStateService.muteRingtone();
+				return true;
+			}
+		}
+		return super.onKeyDown(keyCode, event);
+	}
+
 	//endregion
 
 	//region UI helpers

+ 4 - 4
app/src/main/java/ch/threema/app/voip/services/VoipCallService.java

@@ -80,13 +80,13 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
 import androidx.preference.PreferenceManager;
 import ch.threema.annotation.SameThread;
 import ch.threema.app.BuildConfig;
-import ch.threema.app.push.PushService;
 import ch.threema.app.R;
 import ch.threema.app.exceptions.FileSystemNotPresentException;
 import ch.threema.app.managers.ListenerManager;
 import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.notifications.BackgroundErrorNotification;
 import ch.threema.app.notifications.NotificationBuilderWrapper;
+import ch.threema.app.push.PushService;
 import ch.threema.app.services.ContactService;
 import ch.threema.app.services.NotificationService;
 import ch.threema.app.services.PreferenceService;
@@ -208,7 +208,7 @@ public class VoipCallService extends LifecycleService implements PeerConnectionC
 
 	// Listeners
 	private VoipMessageListener voipMessageListener;
-	private PhoneStateListener hangUpRtcOnDeviceCallAnswered = new PSTNCallStateListener();
+	private final PhoneStateListener hangUpRtcOnDeviceCallAnswered = new PSTNCallStateListener();
 
 	// Receivers
 	private IncomingMobileCallReceiver incomingMobileCallReceiver;
@@ -240,7 +240,7 @@ public class VoipCallService extends LifecycleService implements PeerConnectionC
 	private static final int ICE_DISCONNECTED_SOUND_TIMEOUT_MS = 1000;
 
 	// Camera handling
-	private @NonNull AtomicBoolean switchCamInProgress = new AtomicBoolean(false);
+	private final AtomicBoolean switchCamInProgress = new AtomicBoolean(false);
 	private final Object capturingLock = new Object(); // Lock whenever modifying capturing
 	private volatile boolean isCapturing = false; // Always synchronize on capturingLock!
 
@@ -251,7 +251,7 @@ public class VoipCallService extends LifecycleService implements PeerConnectionC
 
 	// Broadcast receivers
 	private MeteredStatusChangedReceiver meteredStatusChangedReceiver;
-	private BroadcastReceiver localBroadcastReceiver = new BroadcastReceiver() {
+	private final BroadcastReceiver localBroadcastReceiver = new BroadcastReceiver() {
 		@Override
 		@UiThread
 		public void onReceive(Context context, Intent intent) {

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

@@ -24,9 +24,11 @@ package ch.threema.app.voip.services;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.media.AudioManager;
@@ -144,21 +146,21 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 	private final NotificationManager notificationManager;
 
 	// Threema services
-	private ContactService contactService;
-	private RingtoneService ringtoneService;
-	private PreferenceService preferenceService;
-	private MessageService messageService;
-	private LifetimeService lifetimeService;
+	private final ContactService contactService;
+	private final RingtoneService ringtoneService;
+	private final PreferenceService preferenceService;
+	private final MessageService messageService;
+	private final LifetimeService lifetimeService;
 
 	// Message sending
-	private MessageQueue messageQueue;
+	private final MessageQueue messageQueue;
 
 	// App context
-	private Context appContext;
+	private final Context appContext;
 
 	// State
 	private volatile Boolean initiator = null;
-	private CallState callState = new CallState();
+	private final CallState callState = new CallState();
 	private Long callStartTimestamp = null;
 
 	// Map that stores incoming offers
@@ -190,6 +192,14 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 	private static final int VOIP_CONNECTION_LINGER = 1000 * 5;
 
 	private final WearableHandler wearableHandler;
+	private ScreenOffReceiver screenOffReceiver;
+
+	private class ScreenOffReceiver extends BroadcastReceiver {
+		@Override
+		public void onReceive(Context context, Intent intent) {
+			muteRingtone();
+		}
+	}
 
 	public VoipStateService(ContactService contactService,
 	                        RingtoneService ringtoneService,
@@ -1288,13 +1298,26 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 		}
 	}
 
+	/**
+	 * Mute ringtone if call is in ringing state
+	 */
+	public void muteRingtone() {
+		final CallStateSnapshot currentCallState = this.getCallState();
+		final boolean incoming = this.isInitiator() != Boolean.TRUE;
+
+		if (incoming && currentCallState.isRinging()) {
+			this.stopRingtone();
+			logger.info("Muting ringtone as requested by user");
+		}
+	}
+
 	/**
 	 * Cancel a pending call notification for the specified identity.
 	 */
 	void cancelCallNotification(@NonNull String identity) {
 		// Cancel fullscreen activity launched by notification first
 		VoipUtil.sendVoipBroadcast(appContext, CallActivity.ACTION_CANCELLED);
-		ThreemaApplication.getAppContext().stopService(new Intent(ThreemaApplication.getAppContext(), VoipCallService.class));
+		appContext.stopService(new Intent(ThreemaApplication.getAppContext(), VoipCallService.class));
 
 		this.stopRingtone();
 
@@ -1306,7 +1329,11 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 			} else {
 				logger.warn("No call notification found for {}", identity);
 			}
+			if (this.callNotificationTags.size() == 0) {
+				unregisterScreenOffReceiver();
+			}
 		}
+
 		if (PushService.playServicesInstalled(appContext)){
 			WearableHandler.cancelOnWearable(TYPE_NOTIFICATION);
 			WearableHandler.cancelOnWearable(TYPE_ACTIVITY);
@@ -1324,9 +1351,26 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 			}
 			this.callNotificationTags.clear();
 		}
+
 		if (PushService.playServicesInstalled(appContext)){
 			WearableHandler.cancelOnWearable(TYPE_NOTIFICATION);
 		}
+
+		unregisterScreenOffReceiver();
+	}
+
+	private void registerScreenOffReceiver() {
+		if (screenOffReceiver == null) {
+			screenOffReceiver = new ScreenOffReceiver();
+			appContext.registerReceiver(screenOffReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
+		}
+	}
+
+	private void unregisterScreenOffReceiver() {
+		if (screenOffReceiver != null) {
+			appContext.unregisterReceiver(screenOffReceiver);
+			screenOffReceiver = null;
+		}
 	}
 
 	/**
@@ -1447,6 +1491,9 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 		if (PushService.playServicesInstalled(appContext)){
 			wearableHandler.showWearableNotification(contact, callId, avatar);
 		}
+
+		// register screen off receiver
+		registerScreenOffReceiver();
 	}
 
 	private void playRingtone(MessageReceiver messageReceiver, boolean isMuted) {

+ 5 - 0
app/src/main/java/ch/threema/app/webclient/converter/Contact.java

@@ -272,6 +272,11 @@ public class Contact extends Converter {
 			public Boolean includeHidden() {
 				return true;
 			}
+
+			@Override
+			public Boolean onlyWithReceiptSettings() {
+				return false;
+			}
 		};
 	}
 

+ 6 - 1
app/src/main/java/ch/threema/app/workers/IdentityStatesWorker.java

@@ -66,7 +66,7 @@ public class IdentityStatesWorker extends Worker {
 	@NonNull
 	@Override
 	public Result doWork() {
-		logger.info("@@@@ Starting IdentityStatesWorker");
+		logger.info("Starting IdentityStatesWorker");
 
 		if (this.contactService == null) {
 			logger.info("ContactService not available while updating IdentityStates");
@@ -104,6 +104,11 @@ public class IdentityStatesWorker extends Worker {
 			public Boolean includeHidden() {
 				return true;
 			}
+
+			@Override
+			public Boolean onlyWithReceiptSettings() {
+				return false;
+			}
 		});
 
 		if (contactModelList != null && contactModelList.size() > 0) {

+ 25 - 4
app/src/main/java/ch/threema/client/AbstractMessage.java

@@ -30,11 +30,11 @@ import org.slf4j.LoggerFactory;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.io.UnsupportedEncodingException;
 import java.nio.charset.StandardCharsets;
 import java.security.SecureRandom;
 import java.util.Date;
 
+import ch.threema.base.Contact;
 import ch.threema.base.ThreemaException;
 import ch.threema.client.ballot.BallotCreateMessage;
 import ch.threema.client.ballot.BallotData;
@@ -45,7 +45,16 @@ import ch.threema.client.ballot.GroupBallotVoteMessage;
 import ch.threema.client.file.FileData;
 import ch.threema.client.file.FileMessage;
 import ch.threema.client.file.GroupFileMessage;
-import ch.threema.client.voip.*;
+import ch.threema.client.voip.VoipCallAnswerData;
+import ch.threema.client.voip.VoipCallAnswerMessage;
+import ch.threema.client.voip.VoipCallHangupData;
+import ch.threema.client.voip.VoipCallHangupMessage;
+import ch.threema.client.voip.VoipCallOfferData;
+import ch.threema.client.voip.VoipCallOfferMessage;
+import ch.threema.client.voip.VoipCallRingingData;
+import ch.threema.client.voip.VoipCallRingingMessage;
+import ch.threema.client.voip.VoipICECandidatesData;
+import ch.threema.client.voip.VoipICECandidatesMessage;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
@@ -91,7 +100,10 @@ public abstract class AbstractMessage {
 			throw new BadMessageException("Message is not for own identity, cannot decode");
 		}
 
-		/* obtain public key of sender */
+		// check if contact already exists
+		Contact contact = contactStore.getContactForIdentity(boxmsg.getFromIdentity());
+
+		/* obtain public key of sender. note that this creates a new contact if one doesn't exist already for this identity */
 		byte[] senderPublicKey = contactStore.getPublicKeyForIdentity(boxmsg.getFromIdentity(), fetch);
 
 		if (senderPublicKey == null) {
@@ -321,6 +333,15 @@ public abstract class AbstractMessage {
 				groupleavemsg.setGroupId(new GroupId(data, 1 + ProtocolDefines.IDENTITY_LEN));
 				msg = groupleavemsg;
 
+				if (contact == null) {
+					// ignore leave from previously unknown contact
+					Contact newContact = contactStore.getContactForIdentity(boxmsg.getFromIdentity());
+					if (newContact != null) {
+						contactStore.removeContact(newContact);
+						logger.info("Received group leave from unknown identity {}", boxmsg.getFromIdentity());
+						throw new BadMessageException("Received group leave from unknown identity. dropping", true);
+					}
+				}
 				break;
 			}
 
@@ -706,7 +727,7 @@ public abstract class AbstractMessage {
 			}
 
 			default:
-				logger.warn("Unsupported message type {}", type);
+				logger.info("Unsupported message type {}", type);
 				break;
 		}
 

+ 1 - 8
app/src/main/java/ch/threema/client/ContactStoreInterface.java

@@ -23,12 +23,10 @@ package ch.threema.client;
 
 import ch.threema.base.Contact;
 
-import java.util.Collection;
-
 public interface ContactStoreInterface {
 
 	/**
-	 * Obtain the public key for the given identity.
+	 * Obtain the public key for the given identity and creates a contact if it doesn't already exist
 	 *
 	 * @param identity desired identity
 	 * @param fetch if true, attempt to synchronously fetch the key from the server if necessary
@@ -38,14 +36,9 @@ public interface ContactStoreInterface {
 
 	Contact getContactForIdentity(String identity);
 
-	Collection<Contact> getAllContacts();
-
 	void addContact(Contact contact);
 
 	void hideContact(Contact contact, boolean hide);
 
 	void removeContact(Contact contact);
-
-	void addContactStoreObserver(ContactStoreObserver observer);
-	void removeContactStoreObserver(ContactStoreObserver observer);
 }

+ 0 - 3
app/src/main/java/ch/threema/client/ProtocolDefines.java

@@ -90,9 +90,6 @@ public class ProtocolDefines {
 	public static final int MSGTYPE_GROUP_CREATE = 0x4a;
 	public static final int MSGTYPE_GROUP_RENAME = 0x4b;
 	public static final int MSGTYPE_GROUP_LEAVE = 0x4c;
-	public static final int MSGTYPE_GROUP_ADD_MEMBER = 0x4d;
-	public static final int MSGTYPE_GROUP_REMOVE_MEMBER = 0x4e;
-	public static final int MSGTYPE_GROUP_DESTROY = 0x4f;
 	public static final int MSGTYPE_GROUP_SET_PHOTO = 0x50;
 	public static final int MSGTYPE_GROUP_REQUEST_SYNC = 0x51;
 	public static final int MSGTYPE_GROUP_BALLOT_CREATE = 0x52;

+ 5 - 1
app/src/main/java/ch/threema/storage/DatabaseServiceNew.java

@@ -95,6 +95,7 @@ import ch.threema.app.services.systemupdate.SystemUpdateToVersion62;
 import ch.threema.app.services.systemupdate.SystemUpdateToVersion63;
 import ch.threema.app.services.systemupdate.SystemUpdateToVersion64;
 import ch.threema.app.services.systemupdate.SystemUpdateToVersion65;
+import ch.threema.app.services.systemupdate.SystemUpdateToVersion66;
 import ch.threema.app.services.systemupdate.SystemUpdateToVersion7;
 import ch.threema.app.services.systemupdate.SystemUpdateToVersion8;
 import ch.threema.app.services.systemupdate.SystemUpdateToVersion9;
@@ -126,7 +127,7 @@ public class DatabaseServiceNew extends SQLiteOpenHelper {
 	public static final String DATABASE_NAME = "threema.db";
 	public static final String DATABASE_NAME_V4 = "threema4.db";
 	public static final String DATABASE_BACKUP_EXT = ".backup";
-	private static final int DATABASE_VERSION = 65;
+	private static final int DATABASE_VERSION = 66;
 	private final Context context;
 	private final String key;
 	private final UpdateSystemService updateSystemService;
@@ -587,6 +588,9 @@ public class DatabaseServiceNew extends SQLiteOpenHelper {
 		if (oldVersion < 65) {
 			this.updateSystemService.addUpdate(new SystemUpdateToVersion65(this.context));
 		}
+		if (oldVersion < 66) {
+			this.updateSystemService.addUpdate(new SystemUpdateToVersion66(sqLiteDatabase));
+		}
 	}
 
 	public void executeNull() throws SQLiteException {

+ 8 - 2
app/src/main/java/ch/threema/storage/factories/ContactModelFactory.java

@@ -142,7 +142,9 @@ public class ContactModelFactory extends ModelFactory {
 					.setProfilePicSentDate(cursorFactory.getDate(ContactModel.COLUMN_PROFILE_PIC_SENT_DATE))
 					.setDateCreated(cursorFactory.getDate(ContactModel.COLUMN_DATE_CREATED))
 					.setIsRestored(cursorFactory.getInt(ContactModel.COLUMN_IS_RESTORED) == 1)
-					.setArchived(cursorFactory.getInt(ContactModel.COLUMN_IS_ARCHIVED) == 1);
+					.setArchived(cursorFactory.getInt(ContactModel.COLUMN_IS_ARCHIVED) == 1)
+					.setReadReceipts(cursorFactory.getInt(ContactModel.COLUMN_READ_RECEIPTS))
+					.setTypingIndicators(cursorFactory.getInt(ContactModel.COLUMN_TYPING_INDICATORS));
 
 				switch (cursorFactory.getInt(ContactModel.COLUMN_VERIFICATION_LEVEL))
 				{
@@ -217,6 +219,8 @@ public class ContactModelFactory extends ModelFactory {
 		contentValues.put(ContactModel.COLUMN_IS_HIDDEN, contactModel.isHidden());
 		contentValues.put(ContactModel.COLUMN_IS_RESTORED, contactModel.isRestored());
 		contentValues.put(ContactModel.COLUMN_IS_ARCHIVED, contactModel.isArchived());
+		contentValues.put(ContactModel.COLUMN_READ_RECEIPTS, contactModel.getReadReceipts());
+		contentValues.put(ContactModel.COLUMN_TYPING_INDICATORS, contactModel.getTypingIndicators());
 
 		if (insert) {
 			//never update identity field
@@ -267,7 +271,9 @@ public class ContactModelFactory extends ModelFactory {
 						"`" + ContactModel.COLUMN_IS_HIDDEN + "` TINYINT DEFAULT 0," +
 						"`" + ContactModel.COLUMN_IS_RESTORED + "` TINYINT DEFAULT 0," +
 						"`" + ContactModel.COLUMN_IS_ARCHIVED + "` TINYINT DEFAULT 0," +
-						"PRIMARY KEY (`" + ContactModel.COLUMN_IDENTITY + "`) );"
+						"`" + ContactModel.COLUMN_READ_RECEIPTS + "` TINYINT DEFAULT 0," +
+						"`" + ContactModel.COLUMN_TYPING_INDICATORS + "` TINYINT DEFAULT 0," +
+					"PRIMARY KEY (`" + ContactModel.COLUMN_IDENTITY + "`) );"
 		};
 	}
 

+ 54 - 7
app/src/main/java/ch/threema/storage/models/ContactModel.java

@@ -23,8 +23,11 @@ package ch.threema.storage.models;
 
 import android.text.format.DateUtils;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.Date;
 
+import androidx.annotation.IntDef;
 import androidx.annotation.Nullable;
 import ch.threema.base.Contact;
 import ch.threema.base.VerificationLevel;
@@ -40,7 +43,7 @@ public class ContactModel extends Contact implements ReceiverModel {
 	public static final String COLUMN_VERIFICATION_LEVEL = "verificationLevel";
 	public static final String COLUMN_ANDROID_CONTACT_LOOKUP_KEY = "androidContactId"; /* The complete lookup key (consisting of key and ID) of the android contact this contact is linked with */
 	@Deprecated	public static final String COLUMN_THREEMA_ANDROID_CONTACT_ID = "threemaAndroidContactId";
-	public static final String COLUMN_IS_SYNCHRONIZED= "isSynchronized";
+	@Deprecated public static final String COLUMN_IS_SYNCHRONIZED = "isSynchronized";
 	public static final String COLUMN_FEATURE_LEVEL = "featureLevel";
 	public static final String COLUMN_STATE = "state";
 	public static final String COLUMN_COLOR = "color";
@@ -52,6 +55,8 @@ public class ContactModel extends Contact implements ReceiverModel {
 	public static final String COLUMN_IS_HIDDEN = "isHidden"; /* whether this contact is visible in the contact list */
 	public static final String COLUMN_IS_RESTORED = "isRestored"; /* whether this contact has been restored from a backup and not yet been contacted */
 	public static final String COLUMN_IS_ARCHIVED = "isArchived"; /* whether this contact has been archived by user */
+	public static final String COLUMN_READ_RECEIPTS = "readReceipts"; /* whether read receipts should be sent to this contact */
+	public static final String COLUMN_TYPING_INDICATORS = "typingIndicators"; /* whether typing indicators should be sent to this contact */
 
 	public enum State {
 		/**
@@ -75,6 +80,17 @@ public class ContactModel extends Contact implements ReceiverModel {
 		INVALID
 	}
 
+	/**
+	 * Policy for sending read receipts or typing indicators
+	 */
+	public static final int DEFAULT = 0; // use the global setting
+	public static final int SEND = 1; // always send, regardless of global setting
+	public static final int DONT_SEND = 2; // never send
+
+	@Retention(RetentionPolicy.SOURCE)
+	@IntDef({DEFAULT, SEND, DONT_SEND})
+	public @interface OverridePolicy {}
+
 	// Timeout for avatars of linked contacts
 	public static long DEFAULT_ANDROID_CONTACT_AVATAR_EXPIRY = DateUtils.DAY_IN_MILLIS * 14;
 
@@ -88,6 +104,7 @@ public class ContactModel extends Contact implements ReceiverModel {
 	private boolean isWork, isHidden, isRestored, isArchived;
 	private Date avatarExpires, profilePicSent, dateCreated;
 	private int type;
+	private @OverridePolicy int readReceipts, typingIndicators;
 
 	public ContactModel(String identity, byte[] publicKey) {
 		super(identity, publicKey);
@@ -104,6 +121,10 @@ public class ContactModel extends Contact implements ReceiverModel {
 
 	public ContactModel setAndroidContactLookupKey(String androidContactId) {
 		this.androidContactId = androidContactId;
+		// degrade verification level as this contact is no longer connected to an address book contact
+		if (androidContactId == null && getVerificationLevel() == VerificationLevel.SERVER_VERIFIED) {
+			setVerificationLevel(VerificationLevel.UNVERIFIED);
+		}
 		return this;
 	}
 
@@ -145,18 +166,24 @@ public class ContactModel extends Contact implements ReceiverModel {
 	 * Whether the contact is synchronized with the address book, i.e. the information came from an automatic sync
 	 * or it was manually linked
 	 * @return true if the contact in androidContactId was automatically synced, false if it was manually linked or not linked at all.
+	 *
+	 * @Deprecated Use getAndroidContactLookupKey() != null to determine if the contact is synchronized with the address book
 	 */
+	@Deprecated
 	public boolean isSynchronized() {
 		return this.isSynchronized;
 	}
 
+	/**
+	 * Sets whether this contact is synchronized with the address book, either from an automatic sync or manually
+	 * @param isSynchronized
+	 * @return ContactModel for a builder
+	 *
+	 * @Deprecated Use setAndroidContactLookupKey()
+	 */
+	@Deprecated
 	public ContactModel setIsSynchronized(boolean isSynchronized) {
 		this.isSynchronized = isSynchronized;
-
-		// degrade verification level as this contact is no longer connected to an address book contact
-		if (!isSynchronized && getVerificationLevel() == VerificationLevel.SERVER_VERIFIED) {
-			setVerificationLevel(VerificationLevel.UNVERIFIED);
-		}
 		return this;
 	}
 
@@ -259,6 +286,24 @@ public class ContactModel extends Contact implements ReceiverModel {
 		return this;
 	}
 
+	public @OverridePolicy int getReadReceipts() {
+		return readReceipts;
+	}
+
+	public ContactModel setReadReceipts(@OverridePolicy int readReceipts) {
+		this.readReceipts = readReceipts;
+		return this;
+	}
+
+	public @OverridePolicy int getTypingIndicators() {
+		return typingIndicators;
+	}
+
+	public ContactModel setTypingIndicators(@OverridePolicy int typingIndicators) {
+		this.typingIndicators = typingIndicators;
+		return this;
+	}
+
 	public Object[] getModifiedValueCandidates() {
 		return new Object[] {
 			this.getPublicKey(),
@@ -279,7 +324,9 @@ public class ContactModel extends Contact implements ReceiverModel {
 			this.dateCreated,
 			this.isHidden,
 			this.isRestored,
-			this.isArchived
+			this.isArchived,
+			this.readReceipts,
+			this.typingIndicators
 		};
 	}
 

+ 1 - 1
app/src/main/res/drawable/ic_new_badge_filled.xml

@@ -5,7 +5,7 @@
     android:viewportHeight="24">
   <path
       android:pathData="M23,12 L20.56,9.22 20.9,5.54 17.29,4.72 15.4,1.54 12,3 8.6,1.54 6.71,4.72 3.1,5.53 3.44,9.21 1,12 3.44,14.78 3.1,18.47 6.71,19.29 8.6,22.47 12,21l3.4,1.46 1.89,-3.18 3.61,-0.82 -0.34,-3.68z"
-      android:fillColor="@color/messagelist_highlight_color"/>
+      android:fillColor="?attr/colorAccent"/>
   <path
       android:pathData="M8.2097,12.3261 L6.1662,9.4652L5.1445,9.4652L5.1445,14.3697L6.1662,14.3697L6.1662,11.5087l2.0844,2.8609l0.9809,0L9.2315,9.4652l-1.0218,0zM10.0489,14.3697l3.2696,0L13.3185,13.3479L11.275,13.3479L11.275,12.4406l2.0435,0l0,-1.0299l-2.0435,0l0,-0.9155l2.0435,0L13.3185,9.4652l-3.2696,0zM18.0186,9.4652l0,3.6783l-0.9155,0l0,-2.8691l-1.0218,0l0,2.8773L15.1577,13.1517L15.1577,9.4652l-1.0218,0l0,4.087c0,0.4496 0.3678,0.8174 0.8174,0.8174l3.2696,0c0.4496,0 0.8174,-0.3678 0.8174,-0.8174L19.0404,9.4652Z"
       android:strokeWidth="0.8174072"

+ 36 - 45
app/src/main/res/layout/activity_storagemanagement.xml

@@ -173,31 +173,27 @@
 
 				</RelativeLayout>
 
-				<RelativeLayout android:layout_width="match_parent"
-								android:layout_height="wrap_content"
-								android:layout_marginTop="16dp"
-								android:layout_marginBottom="16dp"
-								android:orientation="horizontal">
-
-					<TextView
-							android:id="@+id/delete_explain"
-							android:layout_alignParentLeft="true"
-							android:layout_toLeftOf="@+id/time_spinner"
-							android:layout_centerVertical="true"
-							android:layout_width="wrap_content"
-							android:layout_height="wrap_content"
-							android:textAppearance="?android:textAppearanceMedium"
-							android:text="@string/delete_media_files_time"/>
-
-					<Spinner
-							android:id="@+id/time_spinner"
-							android:layout_alignParentRight="true"
-							android:layout_width="wrap_content"
-							android:layout_height="wrap_content"/>
+				<com.google.android.material.textfield.TextInputLayout
+					style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense.ExposedDropdownMenu"
+					android:layout_width="match_parent"
+					android:layout_height="wrap_content"
+					android:layout_marginTop="16dp"
+					android:layout_marginBottom="8dp"
+					android:hint="@string/delete_media_files_time"
+					app:expandedHintEnabled="false">
 
-				</RelativeLayout>
+					<com.google.android.material.textfield.MaterialAutoCompleteTextView
+						android:id="@+id/time_spinner"
+						android:layout_width="match_parent"
+						android:layout_height="wrap_content"
+						android:focusable="false"
+						android:inputType="none" />
 
-				<com.google.android.material.button.MaterialButton android:id="@+id/delete_button"
+				</com.google.android.material.textfield.TextInputLayout>
+
+				<com.google.android.material.button.MaterialButton
+						style="@style/Threema.Chip.Action"
+						android:id="@+id/delete_button"
 						android:layout_width="wrap_content"
 						android:layout_height="wrap_content"
 						android:text="@string/delete_data"
@@ -236,31 +232,26 @@
 
 				</RelativeLayout>
 
-				<RelativeLayout android:layout_width="match_parent"
-								android:layout_height="wrap_content"
-								android:layout_marginTop="16dp"
-								android:layout_marginBottom="16dp"
-								android:orientation="horizontal">
-
-					<TextView
-							android:id="@+id/delete_messages_explain"
-							android:layout_alignParentLeft="true"
-							android:layout_toLeftOf="@+id/time_spinner_messages"
-							android:layout_centerVertical="true"
-							android:layout_width="wrap_content"
-							android:layout_height="wrap_content"
-							android:textAppearance="?android:textAppearanceMedium"
-							android:text="@string/delete_messages_explain"/>
-
-					<Spinner
-							android:id="@+id/time_spinner_messages"
-							android:layout_alignParentRight="true"
-							android:layout_width="wrap_content"
-							android:layout_height="wrap_content"/>
+				<com.google.android.material.textfield.TextInputLayout
+					style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense.ExposedDropdownMenu"
+					android:layout_width="match_parent"
+					android:layout_height="wrap_content"
+					android:layout_marginTop="16dp"
+					android:layout_marginBottom="8dp"
+					android:hint="@string/delete_messages_explain"
+					app:expandedHintEnabled="false">
 
-				</RelativeLayout>
+					<com.google.android.material.textfield.MaterialAutoCompleteTextView
+						android:id="@+id/time_spinner_messages"
+						android:layout_width="match_parent"
+						android:layout_height="wrap_content"
+						android:focusable="false"
+						android:inputType="none" />
+
+				</com.google.android.material.textfield.TextInputLayout>
 
 				<com.google.android.material.button.MaterialButton
+						style="@style/Threema.Chip.Action"
 						android:id="@+id/delete_button_messages"
 						android:layout_width="wrap_content"
 						android:layout_height="wrap_content"

+ 2 - 0
app/src/main/res/layout/fragment_contacts.xml

@@ -10,6 +10,8 @@
 				 android:layout_height="match_parent"
 				 android:background="?android:windowBackground">
 
+		<include layout="@layout/header_contact_section_work"/>
+
 		<ListView
 				android:id="@android:id/list"
 				android:layout_width="match_parent"

+ 32 - 48
app/src/main/res/layout/fragment_my_id.xml

@@ -74,24 +74,6 @@
 					tools:layout_conversion_absoluteHeight="40dp"
 					tools:layout_conversion_absoluteWidth="40dp" />
 
-				<ImageView
-					android:id="@+id/picrelease_config"
-					android:layout_width="40dp"
-					android:layout_height="40dp"
-					android:layout_marginEnd="16dp"
-					android:background="@drawable/circle_transparent"
-					android:contentDescription="@string/configure"
-					android:rotation="180"
-					android:scaleType="center"
-					app:layout_constraintBottom_toBottomOf="@+id/picrelease_spinner"
-					app:layout_constraintEnd_toEndOf="parent"
-					app:layout_constraintTop_toTopOf="@+id/picrelease_spinner"
-					app:srcCompat="@drawable/ic_settings_outline_24dp"
-					app:tint="?attr/colorAccent"
-					tools:layout_conversion_absoluteHeight="40dp"
-					tools:layout_conversion_absoluteWidth="40dp" />
-
-
 				<ImageView
 					android:id="@+id/my_id_qr"
 					android:layout_width="wrap_content"
@@ -138,20 +120,6 @@
 					tools:layout_conversion_absoluteHeight="16dp"
 					tools:layout_conversion_absoluteWidth="366dp" />
 
-
-				<TextView
-					android:id="@+id/picrelease_text"
-					android:layout_width="wrap_content"
-					android:layout_height="wrap_content"
-					android:layout_marginTop="8dp"
-					android:text="@string/profile_picture_release"
-					android:textAppearance="@style/Threema.Text.Overline"
-					android:textColor="@null"
-					app:layout_constraintStart_toStartOf="@+id/nickname"
-					app:layout_constraintTop_toBottomOf="@+id/profile_edit"
-					tools:layout_conversion_absoluteHeight="16dp"
-					tools:layout_conversion_absoluteWidth="366dp" />
-
 				<TextView
 					android:id="@+id/my_id_title"
 					android:layout_width="wrap_content"
@@ -171,28 +139,44 @@
 					tools:layout_editor_absoluteX="16dp"
 					tools:layout_editor_absoluteY="16dp" />
 
-				<androidx.appcompat.widget.AppCompatSpinner
-					android:id="@+id/picrelease_spinner"
-					style="@style/Base.Widget.AppCompat.Spinner"
-					android:layout_width="wrap_content"
+				<ImageView
+					android:id="@+id/picrelease_config"
+					android:layout_width="40dp"
 					android:layout_height="40dp"
-					app:layout_constraintStart_toStartOf="@+id/picrelease_text"
-					app:layout_constraintTop_toBottomOf="@+id/picrelease_text"
-					tools:layout_conversion_absoluteHeight="48dp"
-					tools:layout_conversion_absoluteWidth="48dp" />
+					android:layout_marginEnd="16dp"
+					android:layout_marginTop="4dp"
+					android:background="@drawable/circle_transparent"
+					android:contentDescription="@string/configure"
+					android:rotation="180"
+					android:scaleType="center"
+					app:layout_constraintBottom_toBottomOf="@+id/picrelease_text"
+					app:layout_constraintEnd_toEndOf="parent"
+					app:layout_constraintTop_toTopOf="@+id/picrelease_text"
+					app:srcCompat="@drawable/ic_settings_outline_24dp"
+					app:tint="?attr/colorAccent" />
 
-				<androidx.constraintlayout.widget.ConstraintLayout
-					android:id="@+id/constraintLayout2"
-					android:layout_width="wrap_content"
+				<com.google.android.material.textfield.TextInputLayout
+					android:id="@+id/picrelease_text"
+					style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense.ExposedDropdownMenu"
+					android:layout_width="match_parent"
 					android:layout_height="wrap_content"
+					android:layout_marginTop="8dp"
 					android:layout_marginStart="16dp"
-					android:layout_marginTop="16dp"
+					android:layout_marginEnd="72dp"
+					android:hint="@string/profile_picture_release"
+					app:expandedHintEnabled="false"
 					app:layout_constraintStart_toStartOf="parent"
-					app:layout_constraintTop_toTopOf="parent"
-					tools:layout_conversion_absoluteHeight="48dp"
-					tools:layout_conversion_absoluteWidth="366dp">
+					app:layout_constraintEnd_toEndOf="parent"
+					app:layout_constraintTop_toBottomOf="@+id/nickname">
+
+					<com.google.android.material.textfield.MaterialAutoCompleteTextView
+						android:id="@+id/picrelease_spinner"
+						android:layout_width="match_parent"
+						android:layout_height="wrap_content"
+						android:focusable="false"
+						android:inputType="none" />
 
-				</androidx.constraintlayout.widget.ConstraintLayout>
+				</com.google.android.material.textfield.TextInputLayout>
 
 				<ch.threema.app.emojis.EmojiTextView
 					android:id="@+id/my_id"

+ 1 - 1
app/src/main/res/layout/fragment_wizard1.xml

@@ -71,7 +71,7 @@
 			style="@style/WizardHintedEditText"
 			android:layout_width="match_parent"
 			android:layout_height="@dimen/wizard_hinted_edittext_height"
-			android:imeOptions="actionDone"
+			android:imeOptions="actionGo"
 			android:inputType="text|textNoSuggestions|textPassword"
 			android:nextFocusForward="@+id/safe_password1"
 			android:singleLine="true"

+ 1 - 1
app/src/main/res/layout/fragment_wizard2.xml

@@ -48,7 +48,7 @@
 				android:layout_width="match_parent"
 				android:layout_height="@dimen/wizard_default_view_height"
 				android:hint="@string/new_wizard_hint_enter_nickname"
-				android:imeOptions="actionDone"
+				android:imeOptions="actionGo"
 				android:inputType="textNoSuggestions"/>
 
 	</LinearLayout>

+ 45 - 6
app/src/main/res/layout/header_contact_detail.xml

@@ -16,7 +16,7 @@
 		<ch.threema.app.ui.SectionHeaderView
 				android:layout_width="match_parent"
 				android:layout_height="wrap_content"
-				android:paddingTop="10dp"
+				android:layout_marginTop="10dp"
 				android:text="@string/title_threemaid"/>
 
 		<LinearLayout
@@ -25,7 +25,6 @@
 				android:layout_height="wrap_content"
 				android:orientation="horizontal"
 				android:layout_marginTop="10dp"
-				android:layout_marginBottom="7dp"
 				android:clickable="true">
 
 			<TextView
@@ -57,7 +56,7 @@
 		<ch.threema.app.ui.SectionHeaderView
 				android:layout_width="match_parent"
 				android:layout_height="wrap_content"
-				android:paddingTop="5dp"
+				android:layout_marginTop="12dp"
 				android:text="@string/title_keyfingerprint"/>
 
 		<TextView
@@ -65,10 +64,9 @@
 				android:layout_width="match_parent"
 				android:layout_height="wrap_content"
 				android:textAppearance="@style/Threema.TextAppearance.List.FirstLine"
-				android:paddingTop="10dp"
+				android:layout_marginTop="10dp"
 				android:singleLine="false"/>
 
-
 		<LinearLayout
 				android:id="@+id/nickname_container"
 				android:orientation="vertical"
@@ -133,10 +131,52 @@
 
 		</LinearLayout>
 
+		<ch.threema.app.ui.SectionHeaderView
+			android:id="@+id/privacy_header"
+			android:layout_width="match_parent"
+			android:layout_height="wrap_content"
+			android:paddingTop="10dp"
+			android:text="@string/prefs_privacy" />
+
+		<com.google.android.material.textfield.TextInputLayout
+			style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense.ExposedDropdownMenu"
+			android:layout_width="match_parent"
+			android:layout_height="wrap_content"
+			android:layout_marginTop="10dp"
+			android:hint="@string/prefs_title_read_receipts"
+			app:expandedHintEnabled="false">
+
+			<com.google.android.material.textfield.MaterialAutoCompleteTextView
+				android:id="@+id/read_receipts_spinner"
+				android:layout_width="match_parent"
+				android:layout_height="wrap_content"
+				android:focusable="false"
+				android:inputType="none" />
+
+		</com.google.android.material.textfield.TextInputLayout>
+
+		<com.google.android.material.textfield.TextInputLayout
+			style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense.ExposedDropdownMenu"
+			android:layout_width="match_parent"
+			android:layout_height="wrap_content"
+			android:layout_marginTop="5dp"
+			android:hint="@string/prefs_title_typing_indicator"
+			app:expandedHintEnabled="false">
+
+			<com.google.android.material.textfield.MaterialAutoCompleteTextView
+				android:id="@+id/typing_indicators_spinner"
+				android:layout_width="match_parent"
+				android:layout_height="wrap_content"
+				android:focusable="false"
+				android:inputType="none" />
+
+		</com.google.android.material.textfield.TextInputLayout>
+
 		<LinearLayout
 				android:id="@+id/group_members_title_container"
 				android:layout_width="match_parent"
 				android:layout_height="wrap_content"
+				android:layout_marginTop="12dp"
 				android:layout_marginBottom="5dp"
 				android:orientation="vertical"
 				android:visibility="visible">
@@ -144,7 +184,6 @@
 			<ch.threema.app.ui.SectionHeaderView
 					android:layout_width="match_parent"
 					android:layout_height="wrap_content"
-					android:layout_marginTop="10dp"
 					android:text="@string/group_membership_title"/>
 
 		</LinearLayout>

+ 5 - 36
app/src/main/res/layout/header_contact_section_work.xml

@@ -2,40 +2,9 @@
   ~ Copyright (c) 2021 Threema GmbH
   ~ All rights reserved.
   -->
-<androidx.constraintlayout.widget.ConstraintLayout
+<ViewStub
 	xmlns:android="http://schemas.android.com/apk/res/android"
-	xmlns:app="http://schemas.android.com/apk/res-auto"
-	android:layout_width="match_parent"
-	android:layout_height="40dp"
-	android:paddingLeft="@dimen/listitem_contacts_margin_left_right"
-	android:paddingRight="@dimen/listitem_contacts_margin_left_right"
-	android:background="?attr/background_secondary">
-
-	<com.google.android.material.tabs.TabLayout
-		android:id="@+id/tab_layout"
-		android:layout_width="match_parent"
-		android:layout_height="36dp"
-		android:elevation="4dp"
-		app:layout_constraintLeft_toLeftOf="parent"
-		app:layout_constraintRight_toRightOf="parent"
-		app:layout_constraintTop_toTopOf="parent"
-		app:tabGravity="fill"
-		app:tabMode="fixed">
-
-		<com.google.android.material.tabs.TabItem
-			android:id="@+id/tab_all_contacts"
-			android:layout_width="wrap_content"
-			android:layout_height="wrap_content"
-			android:icon="@drawable/ic_person_outline"
-			android:contentDescription="Alle Kontakte" />
-
-		<com.google.android.material.tabs.TabItem
-			android:id="@+id/tab_work_contacts"
-			android:layout_width="wrap_content"
-			android:layout_height="wrap_content"
-			android:icon="@drawable/ic_work_outline"
-			android:contentDescription="Work-Kontakte" />
-
-	</com.google.android.material.tabs.TabLayout>
-
-</androidx.constraintlayout.widget.ConstraintLayout>
+	android:id="@+id/work_contacts_tab_layout"
+	android:layout_width="0dp"
+	android:layout_height="0dp"
+/>

+ 1 - 1
app/src/main/res/layout/item_contact_list.xml

@@ -81,7 +81,7 @@
 		app:layout_constraintVertical_chainStyle="packed"
 		app:layout_constraintHorizontal_chainStyle="packed"
 		app:layout_constraintHorizontal_bias="0"
-		app:layout_constrainedWidth="true"/>
+		app:layout_constrainedWidth="false"/>
 	<!-- textColor="@null" -> https://stackoverflow.com/a/45198884 -->
 
 	<ch.threema.app.emojis.EmojiTextView

+ 6 - 5
app/src/main/res/layout/popup_identity.xml

@@ -49,10 +49,10 @@
 				android:contentDescription="@string/share_via"
 				android:focusable="true"
 				android:padding="4dp"
-				android:tint="?attr/textColorSecondary"
 				app:layout_constraintLeft_toRightOf="@+id/identity_label"
 				app:layout_constraintTop_toTopOf="@+id/identity_label"
-				app:srcCompat="@drawable/ic_share_outline" />
+				app:srcCompat="@drawable/ic_share_outline"
+				app:tint="?attr/textColorSecondary" />
 
 			<ImageView
 				android:id="@+id/qr_image"
@@ -125,7 +125,8 @@
 				android:text="@string/webclient"
 				android:textAppearance="@style/Threema.TextAppearance.Body2"
 				app:layout_constraintTop_toBottomOf="@id/qr_image"
-				app:layout_constraintLeft_toRightOf="@+id/web_enable" />
+				app:layout_constraintLeft_toRightOf="@+id/web_enable"
+				app:layout_constraintBottom_toBottomOf="parent" />
 
 			<androidx.constraintlayout.widget.Group
 				android:id="@+id/web_controls"
@@ -146,7 +147,7 @@
 		android:layout_gravity="left|top"
 		android:layout_marginLeft="@dimen/identity_popup_arrow_margin_left"
 		android:scaleType="fitXY"
-		android:tint="?attr/background_identity_popup"
-		app:srcCompat="@drawable/triangle_up" />
+		app:srcCompat="@drawable/triangle_up"
+		app:tint="?attr/background_identity_popup" />
 
 </FrameLayout>

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

@@ -151,7 +151,7 @@ els missatges.</string>
     <string name="verify_failed_not_linked">Ha fallat la verificació del vostre número de mòbil. El procés de verificació s\'ha cancel·lat.</string>
     <string name="check_incoming_sms">Comprovant els SMS entrants</string>
     <string name="backup_title">Exportar l\'ID</string>
-    <string name="backup_sum">Exportar el vostre ID de Threema</string>
+    <string name="backup_sum">Exportar la vostra ID de Threema</string>
     <string name="backup_and_delete">Còpia de seguretat i eliminació</string>
     <string name="delete_id_title">Eliminar ID</string>
     <string name="delete_id_message">A menys que hàgiu creat una còpia de seguretat o exportat i desat una còpia d\'aquesta ID mai més podreu enviar o rebre missatges amb aquesta ID.\n\nSi no voleu tornar a utilitzar aquesta ID hauríeu d\'eliminar totes les adreces de correu electrònic i números de telèfon enllaçats abans d\'esborrar-la.</string>
@@ -504,7 +504,7 @@ de contacte</string>
     <string name="kick_user_from_group">Expulsar %1$s del grup</string>
     <string name="show_as_qrcode">Mostrar com a codi QR</string>
     <string name="qr_code">Codi QR</string>
-    <string name="really_leave_id_export">Si encara no ho heu fet, deseu la cadena de text de l\'exportació de la vostra ID o el corresponent codi QR en un lloc segur o imprimiu-lo. El vostre ID de Threema no es pot restaurar sense una còpia de seguretat.</string>
+    <string name="really_leave_id_export">Si encara no ho heu fet, deseu la cadena de text de l\'exportació de la vostra ID o el corresponent codi QR en un lloc segur o imprimiu-lo. La vostra ID de Threema no es pot restaurar sense una còpia de seguretat.</string>
     <string name="revocation_key_title">Revocació d\'ID</string>
     <string name="revocation_key_not_set">No s\'ha definit la contrasenya de revocació</string>
     <string name="revocation_key_set_at">Contrasenya definida %1$s</string>
@@ -1042,13 +1042,14 @@ donar només el vostre nom o un pseudònim. Si no definiu un sobrenom utilitzare
     <string name="continue_recording">Continuar gravant</string>
     <string name="whatsnew_title">What\'s new in %s 4.56?</string>
     <string name="whatsnew_headline">The contact synchronization has been completely rewritten for increased performance and stability. As a consequence, manual linking of system contacts had to be removed and a separate validation option in settings is now longer necessary.\n\nIf you notice any problems with names or avatars of synchronized contacts or other unexpected behavior, please contact us through the usual bug reporting channels.\n\nThanks for testing %s!</string>
+    <string name="whatsnew2_title">Què hi ha de nou?</string>
     <string name="tooltip_identity_popup">Toqueu aquí per mostrar ràpidament la vostra ID de Threema o per escanejar la ID d\'altra gent</string>
     <string name="tap_to_start">Toqueu aquí per iniciar %s ara.</string>
     <string name="two_years">2 anys</string>
     <string name="invalid_backup_path">Camí de còpia no vàlid</string>
     <string name="backup_data_no_permission">No es pot escriure en aquest directori. Escolliu-ne un altre.</string>
-    <string name="prefs_sum_show_unread_badge">Mostrar un distintiu indicant el nombre de missatges no llegits al costat de la icona de missatges</string>
-    <string name="prefs_title_show_unread_badge">Distintiu de missatges no llegits</string>
+    <string name="prefs_sum_show_unread_badge">Mostrar insígnies a les pestanyes de navegació inferiors</string>
+    <string name="prefs_title_show_unread_badge">Insígnies</string>
     <string name="pinning_not_trusted">Error de Fixació de Certificat. Comproveu si \"Entrust Root Certification Authority - G2\" està instal·lada i activada a l\'emmagatzematge de credencials del vostre dispositiu.</string>
     <string name="pinning_failed">Error de Fixació de Certificat. Possible atac \"Man-in-the-middle\". Si teniu un bloquejador d\'anuncis, filtre de contingut, o aplicació de tallafocs instal·lades com \"AdGuard\", desactiveu-los per a Threema.</string>
     <string name="open_myid_popup">Obrir la finestra emergent d\'accés ràpid</string>
@@ -1213,5 +1214,13 @@ donar només el vostre nom o un pseudònim. Si no definiu un sobrenom utilitzare
     <string name="label_continue">Continuar</string>
     <string name="select_date">Seleccioneu una data</string>
     <string name="select_time">Seleccioneu una hora</string>
-    <string name="recipients">Destinataris: %s</string>
+    <string name="send_to">Enviar a %s</string>
+    <string name="receipts_override_choice_send">Enviar</string>
+    <string name="receipts_override_choice_dont_send">No enviar</string>
+    <string name="receipts_override_choice_default">Predeterminat (%s)</string>
+    <string name="unable_to_determine_recording_length">Gravació buida o no se\'n pot determinar la longitud</string>
+    <string name="prefs_header_receipts">Confirmacions</string>
+    <string name="prefs_title_reset_receipts">Netejar configuracions individuals</string>
+    <string name="prefs_sum_reset_receipts">Restablir les configuracions específiques del contacte per confirmacions i indicador d\'escriptura a les predeterminades</string>
+    <string name="reset_successful">Correctament restablertes a predeterminades</string>
 </resources>

+ 34 - 23
app/src/main/res/values-cs/strings.xml

@@ -198,7 +198,7 @@
     <string name="try_again">Zkusit znovu</string>
     <string name="decoding_message">Dešifrování zprávy</string>
     <string name="invalid_barcode">Neplatný typ čárového kódu</string>
-    <string name="expired_barcode">Tento kód vypršel. Znovu jej skenovat znovu přímo z aplikace druhého uživatele.</string>
+    <string name="expired_barcode">Tento kód expiroval. Naskenujte jej prosím znovu přímo z aplikace druhého uživatele.</string>
     <string name="pending">Čekám</string>
     <string name="unlinking_email">Odpojení e‑mailu</string>
     <string name="unlink">Odpojit</string>
@@ -273,7 +273,7 @@ klíč uzamčen</string>
     <string name="prefs_sum_pin_grace">Čas do aktivace zámku obrazovky</string>
     <string name="click_here_to_change_pin">PIN nastaven. Klepněte sem pro jeho změnu</string>
     <string name="set_pin_menu_title">Nastavit nový PIN</string>
-    <string name="set_pin_summary_intro">Ochrana soukromí nastavením PIN kódu (pouze číslice). Tento PIN kód lze použít k zablokování přístupu k uživatelskému rozhraní aplikace Threema po uplynutí určené doby nebo k ochraně soukromých chatů</string>
+    <string name="set_pin_summary_intro">Ochraňte své soukromí nastavením PIN kódu (pouze číslice). Tento PIN kód lze použít k zablokování přístupu k uživatelskému rozhraní aplikace Threema po uplynutí zvolené doby nebo k zabezpečení soukromých konverzací</string>
     <string name="set_pin_again_summary">Zadejte PIN znovu</string>
     <string name="set_pin_hint">PIN</string>
     <string name="title_addgroup">Nová skupina</string>
@@ -338,7 +338,7 @@ klíč uzamčen</string>
     <string name="prefs_title_black_list">Ignorovaná ID</string>
     <string name="prefs_sum_black_list">Zprávy od zde uvedených ID budou ignorovány.</string>
     <string name="verified">Ověřený</string>
-    <string name="want_to_add_to_exclude_list">Tento kontakt je propojen s adresářem telefonu. Jestli ho smažete, v Threema se objeví znovu po synchronizaci kontaktů.\nChcete ho vyloučit ze synchronizace?</string>
+    <string name="want_to_add_to_exclude_list">Tento kontakt je propojen s adresářem telefonu. Jestliže ho smažete, v aplikaci Threema se objeví znovu po synchronizaci kontaktů.\nPřejete si ho vyloučit ze synchronizace?</string>
     <string name="no">Ne</string>
     <string name="yes">Ano</string>
     <string name="deleting_contact">Mažu kontakt</string>
@@ -371,7 +371,7 @@ klíč uzamčen</string>
     <string name="prefs_sum_validate_contacts">Ověří provázanost Threema kontaktů s Android adresářem</string>
     <string name="prefs_title_fullscreen_ime">Celoobrazovková klávesnice</string>
     <string name="prefs_sum_fullscreen_ime_on">Pokud je zařízení v poloze na šířku, klávesnice zakrývá celou plochu. Zobrazen je pouze text psané zprávy.</string>
-    <string name="prefs_sum_fullscreen_ime_off">Pokud je místo, zobrazí náhled chatu nad klávesnicí</string>
+    <string name="prefs_sum_fullscreen_ime_off">Pokud zbývá místo, zobrazí se náhled konverzace nad klávesnicí</string>
     <string name="add_acount_from_within_threema">"Upravuje Vaše změny související s Threema ID
 "</string>
     <string name="save_image">Uložit obrázek</string>
@@ -476,7 +476,7 @@ http://www.7-zip.org or https://itunes.apple.com/us/app/the-unarchiver/id4254243
     <string name="contact_state_inactive">Neaktivní</string>
     <string name="contact_state_invalid">Chybný</string>
     <string name="back">Zpět</string>
-    <string name="wearable_reply">Odpověď</string>
+    <string name="wearable_reply">Odpovědět</string>
     <string name="wearable_reply_label">Odpovědět %s</string>
     <string name="message_acknowledged">Souhlas odeslán</string>
     <string name="push_disable_text">Pokud volbu potvrdíte, zprávy push budou zakázány a Threema bude kontrolovat nové zprávy každých 15 minut.</string>
@@ -491,7 +491,7 @@ http://www.7-zip.org or https://itunes.apple.com/us/app/the-unarchiver/id4254243
     <string name="kick_user_from_group">Vyloučit ze skupiny člena „%1$s“</string>
     <string name="show_as_qrcode">Zobrazit jako QR kód</string>
     <string name="qr_code">QR kód</string>
-    <string name="really_leave_id_export">Pokud jste tak ještě neučinili, uložte záložní řetězec ID nebo 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 nijak obnovena bez Vaší zálohy.</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">Heslo pro odvolání ID není zadáno</string>
     <string name="revocation_key_set_at">Heslo pro %1$s</string>
@@ -648,7 +648,7 @@ zadat pouze vaše křestní jméno nebo pseudonym. Pokud nenastavíte žádnou p
     <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">Neobdrželi jste SMS?</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="verification_of">Ověření %s</string>
     <string name="status_ballot_voting_changed">V anketě „%1$s“ přibyl další hlas</string>
@@ -674,7 +674,7 @@ zadat pouze vaše křestní jméno nebo pseudonym. Pokud nenastavíte žádnou p
     <string name="select_all">Vybrat vše</string>
     <string name="deleting_messages">Mazání zpráv</string>
     <string name="media_gallery_files">Soubory</string>
-    <string name="prefs_gif_autoplay">Automaticky přehrávat animované GIFy</string>
+    <string name="prefs_gif_autoplay">Autom. 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>
@@ -727,7 +727,7 @@ zadat pouze vaše křestní jméno nebo pseudonym. Pokud nenastavíte žádnou p
     <string name="really_reset_ringtones">Skutečně si přejete obnovit nastavení zvuků oznámení na výchozí hodnoty?</string>
     <string name="reset_ringtones_confirm">Nastavení zvuků oznámení bylo obnoveno.</string>
     <string name="prefs_language_override">Jazyk</string>
-    <string name="threema_channel_intro">Informační kanál Threemy nabízí zprávy o všech novinkách. Chcete se přihlásit k odběru kanálu a přidat jej do svých kontaktů? Je zdarma a můžete se kdykoliv odhlásit.</string>
+    <string name="threema_channel_intro">Threema Channel je informační kanál aplikace Threema, který vás formou zpráv informuje o všech novinkách. Přejete si přihlásit se k odběru kanálu a přidat jej mezi kontakty? Je zdarma a můžete se z něj kdykoliv odhlásit.</string>
     <string name="quote">Citovat</string>
     <string name="really_delete_contacts_message" tools:ignore="PluralsCandidate">Skutečně si přejete odstranit následující počet kontaktů: %1$d a všechny k nim přidružené konverzace?</string>
     <string name="contacts_deleted">Kontakty byly smazány</string>
@@ -742,13 +742,13 @@ zadat pouze vaše křestní jméno nebo pseudonym. Pokud nenastavíte žádnou p
     <string name="check_now">Zkontrolovat</string>
     <string name="discard">Odstranit</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í spořiče baterie</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 aplikace %2$s.</string>
     <string name="battery_optimizations_disable_guide">V rozbalovací nabídce vyberte možnost „Všechny aplikace“</string>
-    <string name="battery_optimizations_disable_guide_ctd">Vyhledejte %s v seznamu a vypněte optimalizaci</string>
-    <string name="battery_optimizations_disable_confirm">Skutečně si přejete pro aplikaci %1$s ponechat optimalizaci baterie zapnutou? %2$s nebude pracovat správně.</string>
+    <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">Vložte text</string>
-    <string name="backup_explain_text">Pokud se mobil rozbije nebo ho ztratíte, nikdo již nedokáže obnovit Vaše Threema ID ani vlastní konverzace bez záložních dat. Pravidelně zálohujte a ukládejte zálohovaná data na bezpečné místo.</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="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">Editace</string>
     <string name="edit">Upravit</string>
@@ -817,7 +817,7 @@ zadat pouze vaše křestní jméno nebo pseudonym. Pokud nenastavíte žádnou p
     <string name="notifications_disabled_settings">Upravte nastavení systému</string>
     <string name="error_attaching_files">Přílohy se nezdařilo načíst</string>
     <string name="prefs_fix_powermanager_problems">Vypnutí úsporného režimu</string>
-    <string name="prefs_fix_powermanager_problems_desc">Povolit aplikaci Threema běh na pozadí, pokud nepřijímá zprávy</string>
+    <string name="prefs_fix_powermanager_problems_desc">Povolit aplikaci Threema běžet na pozadí, aby mohla přijímat zprávy i když není aktivní (na popředí).</string>
     <string name="disable_powermanager_explain">Na následující obrazovce se ujistěte, že «%s» je nemonitorovaná (vyloučená) nebo chráněna z omezení správy napájení vašeho telefonu. Až budete hotovi, klepněte na tlačítko «zpět».</string>
     <string name="disable_autostart_explain">Ujistěte se, že na následující obrazovce je aplikace „%s“ zahrnuta v seznamu aplikací, které se spouští automaticky. Jakmile budete hotovi, klepněte na tlačítko „zpět“.</string>
     <string name="notification_priority_default">Nízká</string>
@@ -884,7 +884,7 @@ zadat pouze vaše křestní jméno nebo pseudonym. Pokud nenastavíte žádnou p
     <string name="safe_disable_confirm">Skutečně si přejete pokračovat bez aktivace Threema Safe?</string>
     <string name="safe_configure_choose_password">Zvolte si prosím silné heslo. Toto heslo budete v budoucnu potřebovat k obnovení dat ze zálohy Threema Safe.</string>
     <string name="safe_configure_choose_server">Vyberte Threema Safe server</string>
-    <string name="safe_configure_server_explain">Můžete použít server společnosti Threema nebo server třetí strany.</string>
+    <string name="safe_configure_server_explain">Můžete použít server společnosti Threema nebo pro zálohy určit server třetí strany.</string>
     <string name="safe_use_default_server">Použít výchozí server</string>
     <string name="safe_test_server">Otestovat server</string>
     <string name="safe_advanced_options">Expertní nastavení</string>
@@ -897,12 +897,12 @@ zadat pouze vaše křestní jméno nebo pseudonym. Pokud nenastavíte žádnou p
     <string name="safe_no_backup_found">Na serveru nebyla nalezena žádná záloha. Zkontrolujte ID a heslo.</string>
     <string name="safe_select_id">Bylo nalezeno více dat s tímto Threema ID. Vyberte ID, které chcete použít:</string>
     <string name="safe_backup_now">Zálohovat</string>
-    <string name="safe_enable_explain_short">Aktivace Threema Safe vytvoří automatické zabezpečené a anonymní zálohy všech vašich důležitých dat.</string>
+    <string name="safe_enable_explain_short">Povolením Threema Safe vytvoříte automatické zabezpečené a anonymní zálohy všech vašich důležitých dat.</string>
     <string name="safe_deleting">Vymazává se záloha Threema Safe</string>
     <string name="safe_delete_error">Chyba při mazání zálohy: %s</string>
     <string name="safe_delete_success">Zálohy byly úspěšně smazány ze serveru</string>
     <string name="safe_error_preparing">Chyba během přípravy zálohy Threema Safe</string>
-    <string name="safe_configure_choose_password_force">Zadejte prosím silné heslo pro zabezpečení Vašich dat v Threema Safe záloze. Heslo si opravdu zapamatujte!</string>
+    <string name="safe_configure_choose_password_force">Zadejte prosím silné heslo pro zabezpečení vašeho Threema ID funkcí Threema Safe. Zadané heslo si dobře zapamatujte!</string>
     <string name="safe_deactivate">Deaktivace Threema Safe</string>
     <string name="safe_deactivate_explain">Pokud deaktivujete službu Threema Safe, budou ze serveru odstraněny všechny existující zálohy. Pokračovat?</string>
     <string name="add_group_members">Přidat člena</string>
@@ -968,6 +968,7 @@ zadat pouze vaše křestní jméno nebo pseudonym. Pokud nenastavíte žádnou p
     <string name="unknown">Neznámý</string>
     <string name="miui_notification_title">Důležité upozornění týkající se systému oznámení v systému MIUI</string>
     <string name="miui_notification_body">MIUI 10 standardně vypne zvuk i světelné (LED) upozornění pro všechny nově vytvořené oznamovací kanály (s výjimkou některých aplikací, které společnost Xiaomi pokládá za \"důležité\"). Budete je muset ručně zapnout v nastavení telefonu (sekce oznámení). Další informace získáte od výrobce telefonu.</string>
+    <string name="miui12_notification_body">MIUI ve výchozím nastavení ve všech aplikacích vypíná zvuková, plovoucí a světelná oznámení (kromě některých velmi populárních aplikací považovaných společností Xiaomi za „důležité“). Tato nastavení budete muset zapnout ručně ve vašem zařízení v sekci nastavení oznámení a opakovat tuto proceduru po každé aktualizaci aplikace. Pro více informací prosím kontaktujte společnost Xiaomi.</string>
     <string name="dont_show_again">Příště nezobrazovat</string>
     <string name="miui_notification_prefs">Nastavení MIUI</string>
     <string name="threema_safe_upload_successful">Záloha Threema Safe byla úspěšně nahrána</string>
@@ -1001,7 +1002,7 @@ zadat pouze vaše křestní jméno nebo pseudonym. Pokud nenastavíte žádnou p
     <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">Blízká místa nebyla nalezena</string>
-    <string name="select_directory_for_backup">Uložit zde</string>
+    <string name="select_directory_for_backup">Uložit sem</string>
     <string name="data_backup_headline">Záloha všech dat, včetně konverzací a médií.</string>
     <string name="data_backup_save_path">Cesta pro zálohu dat</string>
     <string name="change">Změnit</string>
@@ -1014,21 +1015,21 @@ zadat pouze vaše křestní jméno nebo pseudonym. Pokud nenastavíte žádnou p
     <string name="no_archived_chats">Nemáte žádné archivované konverzace.\n\nArchivaci konverzace provedete na seznamu konverzací jejím odsunutím doleva</string>
     <string name="add_contact_enter_id_hint">Zadejte Threema ID kontaktu, který chcete přidat</string>
     <string name="notification_channel_new_contact">Nové kontakty</string>
-    <string name="notification_channel_new_contact_desc">Oznámení o nových kontaktech</string>
+    <string name="notification_channel_new_contact_desc">Oznámení o nových kontaktech</string>
     <string name="notification_contact_has_joined">Kontakt %1$s se připojil do aplikace %2$s. Klepnutím sem mu odešlete zprávu.</string>
     <string name="notification_contact_has_joined_multiple">Do aplikace %2$s se připojilo více kontaktů (%1$d): %3$s. Klepnutím sem jim odešlete zprávu.</string>
     <string name="system_default">Systémové nastavení</string>
     <string name="open_in_maps_app">Zobrazit v externí mapě</string>
     <string name="delete">Odstranit</string>
     <string name="num_archived_chats">Archivovaných konverzací: %d</string>
-    <string name="continue_recording">Pokračuje nahrávání</string>
+    <string name="continue_recording">Pokračovat v nahrávání</string>
     <string name="tooltip_identity_popup">Klepnutím sem zobrazíte vaše Threema ID nebo naskenujete ID ostatních uživatelů</string>
     <string name="tap_to_start">Klepnutím sem spustíte aplikaci %s.</string>
     <string name="two_years">2 roky</string>
     <string name="invalid_backup_path">Neplatná cesta pro zálohu dat</string>
     <string name="backup_data_no_permission">Nelze zapisovat do tohoto adresáře. Vyberte jiný.</string>
-    <string name="prefs_sum_show_unread_badge">Vedle ikony aplikace zobrazit puntík s počtem nepřečtených zpráv</string>
-    <string name="prefs_title_show_unread_badge">Indikovat nepřečtené zprávy</string>
+    <string name="prefs_sum_show_unread_badge">Zobrazit ve spodních navigačních záložkách puntíky</string>
+    <string name="prefs_title_show_unread_badge">Puntíky</string>
     <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. Možný útok Man-in-the-middle. Pokud máte nainstalovaný blokovač reklam, filtr obsahu nebo firewall, například „AdGuard“, deaktivujte jej pro Threemu prosím.</string>
     <string name="open_myid_popup">Otevřete vyskakovací okno pro podrobnosti</string>
@@ -1187,9 +1188,19 @@ zadat pouze vaše křestní jméno nebo pseudonym. Pokud nenastavíte žádnou p
     <string name="download_failed">Stažení se nezdařilo. Kód chyby: %d</string>
     <string name="edit_answer">Upravit odpověď</string>
     <string name="share_media">Sdílet v jiné aplikaci…</string>
+    <string name="miui_battery_optimization">Vypněte prosím „optimalizaci baterie“ aplikace %s, aby bylo možné provést tento úkon. Jelikož operační systém od Xiaomi neumožňuje zobrazit obrazovku s nastavením přímo, bude nutné provést úpravy vašeho zařízení ručně v sekci nastavení. Kontaktujte prosím podporu společnosti Xiaomi, jestliže budete potřebovat další pomoc s vypnutím optimalizace baterie.</string>
     <string name="forward_captions">Zahrnout titulky</string>
+    <string name="importing_files_failed">Nezdařilo se importovat soubor se zálohou. Ujistěte se, že je ve vnitřním úložišti zařízení dostatek volného místa.</string>
     <string name="label_continue">Pokračovat</string>
     <string name="select_date">Vyberte datum</string>
     <string name="select_time">Vyberte čas</string>
-    <string name="recipients">Příjemci: %s</string>
+    <string name="send_to">Odeslat kontaktu %s</string>
+    <string name="receipts_override_choice_send">Odeslat</string>
+    <string name="receipts_override_choice_dont_send">Neodesílat</string>
+    <string name="receipts_override_choice_default">Výchozí (%s)</string>
+    <string name="unable_to_determine_recording_length">Prázdné nahrávání nebo nelze určit jeho délku</string>
+    <string name="prefs_header_receipts">Potvrzení</string>
+    <string name="prefs_title_reset_receipts">Vymazat individuální nastavení</string>
+    <string name="prefs_sum_reset_receipts">Obnovit nastavení potvrzení o přečtení a indikaci psaní na výchozí hodnoty</string>
+    <string name="reset_successful">Výchozí nastavení byla úspěšně obnovena</string>
 </resources>

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

@@ -44,7 +44,7 @@
 	</string>
 	<string name="prefs_title_block_unknown">Unbekannte blockieren</string>
 	<string name="prefs_title_read_receipts">Lesebestätigungen senden</string>
-	<string name="prefs_title_typing_indicator">Melden wenn ich tippe</string>
+	<string name="prefs_title_typing_indicator">Melden, wenn ich tippe</string>
 	<string name="prefs_media_title">Medien &amp; Speicher</string>
 	<string name="prefs_sum_media_title">Einstellungen zu Medien, Dateien und Speicherplatz</string>
 	<string name="prefs_image_size">Bild-Abmessungen</string>
@@ -383,7 +383,7 @@
 		von Kontakten ignoriert.
 	</string>
 	<string name="prefs_title_excluded_sync_identities">Ausschlussliste</string>
-	<string name="synchronize_contact">Adressbuch-Synchornoisation</string>
+	<string name="synchronize_contact">Adressbuch-Synchronisation</string>
 	<string name="exclude_contact" tools:ignore="Typos">Von autom. Synchr. ausschliessen</string>
 	<string name="prefs_header_lists">Listen</string>
 	<string name="prefs_title_black_list">Blockierte Kontakte</string>
@@ -1307,6 +1307,13 @@ sicheren Ort gesichert oder ausgedruckt haben.</string>
 	<string name="label_continue">Weiter</string>
 	<string name="select_date">Datum auswälen</string>
 	<string name="select_time">Zeit auswählen</string>
-	<string name="recipients">Empfänger: %s</string>
 	<string name="send_to">Senden an %s</string>
+	<string name="receipts_override_choice_send">Senden</string>
+	<string name="receipts_override_choice_dont_send">Nicht senden</string>
+	<string name="receipts_override_choice_default">Standard (%s)</string>
+	<string name="unable_to_determine_recording_length">Aufnahme leer oder Länge kann nicht bestimmt werden</string>
+	<string name="prefs_header_receipts">Bestätigungen</string>
+	<string name="prefs_title_reset_receipts">Individuelle Einstellungen zurücksetzen</string>
+	<string name="prefs_sum_reset_receipts">Kontaktspezifische Einstellungen für Lesebestätigungen und «Melden wenn ich tippe» auf Standard zurücksetzen</string>
+	<string name="reset_successful">Erfolgreich auf Standardeinstellungen zurückgesetzt</string>
 </resources>

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

@@ -1195,5 +1195,13 @@ almacenado.</string>
     <string name="label_continue">Continuar</string>
     <string name="select_date">Selecciona una fecha</string>
     <string name="select_time">Selecciona una hora</string>
-    <string name="recipients">Destinatarios: %s</string>
+    <string name="send_to">Enviar a %s</string>
+    <string name="receipts_override_choice_send">Enviar</string>
+    <string name="receipts_override_choice_dont_send">No enviar</string>
+    <string name="receipts_override_choice_default">Predeterminado (%s)</string>
+    <string name="unable_to_determine_recording_length">Grabación vacía o imposible determinar su duración</string>
+    <string name="prefs_header_receipts">Confirmaciones</string>
+    <string name="prefs_title_reset_receipts">Borrar ajustes individuales</string>
+    <string name="prefs_sum_reset_receipts">Restablecer ajustes específicos de contactos para confirmaciones de lectura e indicador de escritura a los predeterminados.</string>
+    <string name="reset_successful">Predeterminados restablecidos correctamente</string>
 </resources>

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

@@ -1185,5 +1185,13 @@ Veuillez saisir une question pour votre enquête.</string>
     <string name="label_continue">Continuer</string>
     <string name="select_date">Sélectionner une date</string>
     <string name="select_time">Sélectionner une heure</string>
-    <string name="recipients">Destinataires : %s</string>
+    <string name="send_to">Envoyer à %s</string>
+    <string name="receipts_override_choice_send">Envoyer</string>
+    <string name="receipts_override_choice_dont_send">Ne pas envoyer</string>
+    <string name="receipts_override_choice_default">Par défaut (%s)</string>
+    <string name="unable_to_determine_recording_length">Enregistrement vide ou durée indéterminable</string>
+    <string name="prefs_header_receipts">Accusés de réception</string>
+    <string name="prefs_title_reset_receipts">Effacer les paramètres individuels</string>
+    <string name="prefs_sum_reset_receipts">Rétablir les paramètres spécifiques aux contacts pour les accusés de réception de lecture et l\'indicateur de saisie aux valeurs par défaut</string>
+    <string name="reset_successful">Valeurs par défaut bien rétablies</string>
 </resources>

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

@@ -977,7 +977,7 @@ messaggi ogni 15 minuti.</string>
     <string name="voip_disabled">Le chiamate Threema Safe sono disabilitate</string>
     <string name="hide_chat_enter_message_explain">Questa chat è contrassegnata come privata. Per entrare devi prima impostare la protezione di accesso.</string>
     <string name="unknown">Sconosciuto</string>
-    <string name="miui_notification_title">Avviso importante sulle notifiche in MIUI</string>
+    <string name="miui_notification_title">Avviso importante sulle notifiche in MIUI 10</string>
     <string name="miui_notification_body">Per impostazione predefinita MIUI 10 disattiva i suoni e le luci per tutti i nuovi canali di notifica creati (a eccezione di alcune app che vengono considerate \"importanti\" da Xiaomi). Nel tuo telefono e a partire dalla schermata delle impostazioni di notifica specifica per l\'app dovrai attivarle esplicitamente per ciascun canale. Per maggiori informazioni, contatta il produttore del telefono.</string>
     <string name="miui12_notification_body">Per impostazione predefinita MIUI disattiva i suoni, le luci e le notifiche mobili per tutte le app (a eccezione di alcune app molto popolari che vengono considerate \"importanti\" da Xiaomi). Dovrai attivare manualmente queste impostazioni nella schermata delle impostazioni delle notifiche specifica per l\'app e ripetere la procedura dopo ogni aggiornamento dell\'app. Per maggiori informazioni, contatta Xiaomi.</string>
     <string name="dont_show_again">Non mostrare di nuovo</string>
@@ -1205,5 +1205,4 @@ messaggi ogni 15 minuti.</string>
     <string name="label_continue">Continua</string>
     <string name="select_date">Seleziona una data</string>
     <string name="select_time">Seleziona un\'ora</string>
-    <string name="recipients">Destinatari: %s</string>
 </resources>

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

@@ -1186,5 +1186,13 @@ Weet u zeker dat u Threema anoniem wil gebruiken?</string>
     <string name="label_continue">Doorgaan</string>
     <string name="select_date">Selecteer een datum</string>
     <string name="select_time">Selecteer een tijd</string>
-    <string name="recipients">Ontvanger: %s</string>
+    <string name="send_to">Verzenden naar %s</string>
+    <string name="receipts_override_choice_send">Verzenden</string>
+    <string name="receipts_override_choice_dont_send">Niet verzenden</string>
+    <string name="receipts_override_choice_default">Standaard (%s)</string>
+    <string name="unable_to_determine_recording_length">Lege opname of kan lengte niet bepalen</string>
+    <string name="prefs_header_receipts">Leesbevestigingen</string>
+    <string name="prefs_title_reset_receipts">Individuele instellingen wissen</string>
+    <string name="prefs_sum_reset_receipts">Contactspecifieke instellingen voor leesbevestigingen en invoerindicator naar standaard herstellen</string>
+    <string name="reset_successful">Hersteld naar standaard</string>
 </resources>

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

@@ -1202,5 +1202,13 @@ anonimowo?</string>
     <string name="label_continue">Kontynuuj</string>
     <string name="select_date">Wybierz datę</string>
     <string name="select_time">Wybierz godzinę</string>
-    <string name="recipients">Odbiorcy: %s</string>
+    <string name="send_to">Wysłano do  %s</string>
+    <string name="receipts_override_choice_send">Wyślij</string>
+    <string name="receipts_override_choice_dont_send">Nie wysyłaj</string>
+    <string name="receipts_override_choice_default">Domyślnie (%s)</string>
+    <string name="unable_to_determine_recording_length">Puste nagranie lub niemożność określenia jego długości</string>
+    <string name="prefs_header_receipts">Potwierdzenia</string>
+    <string name="prefs_title_reset_receipts">Usuń indywidualne ustawienia</string>
+    <string name="prefs_sum_reset_receipts">Przywróć domyślne ustawienia specyficzne dla kontaktów i potwierdzeń przeczytania oraz wskaźnika pisania.</string>
+    <string name="reset_successful">Skutecznie przywrócono ustawienia domyślne</string>
 </resources>

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

@@ -1190,5 +1190,13 @@ ficará órfã. Os outros membros ainda poderão conversar, mas não serão mais
     <string name="label_continue">Continuar</string>
     <string name="select_date">Selecione uma data</string>
     <string name="select_time">Selecione um horário</string>
-    <string name="recipients">Destinatários: %s</string>
+    <string name="send_to">Enviar para %s</string>
+    <string name="receipts_override_choice_send">Enviar</string>
+    <string name="receipts_override_choice_dont_send">Não enviar</string>
+    <string name="receipts_override_choice_default">Padrão (%s)</string>
+    <string name="unable_to_determine_recording_length">Gravação vazia ou não foi possível determinar a duração</string>
+    <string name="prefs_header_receipts">Confirmações</string>
+    <string name="prefs_title_reset_receipts">Limpar configurações individuais</string>
+    <string name="prefs_sum_reset_receipts">Restaurar configurações específicas do contato para confirmações de leitura e indicador de digitação para padrão</string>
+    <string name="reset_successful">Restauração para padrão bem-sucedida</string>
 </resources>

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

@@ -954,6 +954,7 @@ Endatescha in pled-clav per tes backup da datas.</string>
     <string name="unknown">Nunenconuschent</string>
     <string name="miui_notification_title">Infurmaziun impurtanta per utilisaders da MIUI 10</string>
     <string name="miui_notification_body">MIUI 10 supprima sco standard tuns, LED e popups per novs chanals d\'infurmaziun (cun excepziun d\'insaquants apps che Xiaomi considerescha sco «impurtants»). En las configuraziuns d\'infurmaziun specificas per Threema stos ti perquai activar quests parameters per tut ils chanals. Contactescha il producent da tes telefonin per ulteriuras infurmaziuns.</string>
+    <string name="miui12_notification_body">MIUI supprima sco standard tuns, LED e popups (cun excepziun d\'insaquantas apps che Xiaomi considerescha sco «impurtantas»). Deplorablamain stos ti perquai activar manualmain questa opziun en las configuraziuns d\'infurmaziun da tes telefonin specificas per Threema – e quai danovamain suenter mintga update da l\'app. Contactescha il producent da tes telefonin per ulteriuras infurmaziuns.</string>
     <string name="dont_show_again">Betg mussar pli</string>
     <string name="miui_notification_prefs">Configuraziuns MIUI</string>
     <string name="threema_safe_upload_successful">Chargià si cun success il backup da Threema Safe</string>
@@ -1170,4 +1171,21 @@ Endatescha in pled-clav per tes backup da datas.</string>
     <string name="tap_here_for_more">Clicca qua per dapli infurmaziuns</string>
     <string name="another_connection_instructions">Il server ha identifitgà dus u plirs apparats colliads cun la medema ID da Threema.\n\nI n\'è betg pussaivel d\'utilisar in\'ID da Threema il medem mument sin plirs apparats. Novs messadis vegnan tramess sulettamain a l\'apparat ch\'è s\'annunzià sco ultim sin il server.\n\nSche ti midas sin in nov apparat, deinstallescha u deactivescha p.pl. %s sin l\'apparat vegl e reaviescha suenter il nov apparat.</string>
     <string name="app_store_error_code">Code dal sbagl da l\'App Store: %d</string>
+    <string name="download_failed">Download betg reussì. Code dal sbagl: %d</string>
+    <string name="share_media">Parter cun in\'autra app...</string>
+    <string name="miui_battery_optimization">P.pl. deactivar l\'optimaziun da l\'accu per %s per pudair duvrar questa funcziun. Cunquai che Xiaomi na considerescha betg sco necessari d\'observar ils standards d\'Android, stos ti far manualmain quella midada en las configuraziuns da tes telefonin. Sche ti dovras agid da deactivar l\'optimaziun da l\'accu, contactescha p.pl. il support da Xiaomi.</string>
+    <string name="forward_captions">Includer las legendas</string>
+    <string name="importing_files_failed">Betg reussì d\'importar la datoteca da backup. Procura per avunda capacitad da l\'arcun intern.</string>
+    <string name="label_continue">Vinavant</string>
+    <string name="select_date">Tscherner la data</string>
+    <string name="select_time">Tscherner il temp</string>
+    <string name="send_to">Trametter a %s</string>
+    <string name="receipts_override_choice_send">Trametter</string>
+    <string name="receipts_override_choice_dont_send">Betg trametter</string>
+    <string name="receipts_override_choice_default">Standard (%s)</string>
+    <string name="unable_to_determine_recording_length">Registraziun vida u betg pussaivel da determinar la lunghezza</string>
+    <string name="prefs_header_receipts">Confermas</string>
+    <string name="prefs_title_reset_receipts">Redefinir configuraziuns individualas</string>
+    <string name="prefs_sum_reset_receipts">"Redefinir il standard per las configuraziuns specificas dals contacts per la conferma da lectura e «Mussar che jau scriv» "</string>
+    <string name="reset_successful">Redefinì cun success las config. da standard</string>
 </resources>

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

@@ -1184,5 +1184,13 @@
     <string name="label_continue">Продолжить</string>
     <string name="select_date">Выберите дату</string>
     <string name="select_time">Выберите время</string>
-    <string name="recipients">Получатели: %s</string>
+    <string name="send_to">Отправить %s</string>
+    <string name="receipts_override_choice_send">Отправить</string>
+    <string name="receipts_override_choice_dont_send">Не отправлять</string>
+    <string name="receipts_override_choice_default">По умолчанию (%s)</string>
+    <string name="unable_to_determine_recording_length">Пустая запись или невозможно определить длину</string>
+    <string name="prefs_header_receipts">Уведомления</string>
+    <string name="prefs_title_reset_receipts">Сбросить индивидуальные настройки</string>
+    <string name="prefs_sum_reset_receipts">Сброс уведомлений о прочтении и индикатора ввода текста для конкретных контактов до настроек по умолчанию</string>
+    <string name="reset_successful">Успешно сброшено до настроек по умолчанию</string>
 </resources>

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

@@ -153,9 +153,8 @@ tekrar denemeden önce girdiğiniz numaranın doğru olduğundan ve mobil ağın
     <string name="backup_sum">Threema ID\'nizi dışa aktarın</string>
     <string name="backup_and_delete">Yedekle ve Sil</string>
     <string name="delete_id_title">Kimliği Sil</string>
-    <string name="delete_id_message">Bir yedek oluşturmadıysanız veya bu kimliğin bir kopyasını dışa aktarıp kaydetmediyseniz, bu kimliğe
-sahip mesajları bir daha gönderemez veya alamazsınız. \ N \ n Bu kimliği tekrar kullanmak istemiyorsanız, bağlı e-posta
-adreslerini / telefon numaralarını silmeden önce.</string>
+    <string name="delete_id_message">Bir yedek oluşturmadığınız veya bu kimliğin bir kopyasını dışa aktarıp kaydetmediğiniz sürece, bir daha asla gönderemez veya tekrar bu kimliğe sahip mesajları.\n\nBu kimliği tekrar kullanmayı düşünmüyorsanız,
+silmeden önce bağlantılı tüm e-posta adreslerini/telefon numaralarını kaldırmalısınız .</string>
     <string name="delete_id_message2">Son uyarı: Bu cihazdaki kimliğinizi gerçekten silmek istiyor musunuz?</string>
     <string name="delete_id_sum">Kimliğinizi ve bu cihazdaki tüm verileri kalıcı olarak silin</string>
     <string name="backup_password_summary">Kimlik dışa aktarma işleminiz bir şifre ile şifrelenir. Harfler, sayılar ve simgelerden oluşan bir kombinasyon kullanın. Buraya ne girdiğinizi unutmayın!</string>
@@ -278,14 +277,14 @@ Kilitlendiğinde yeni mesajlar genel bir bildirimi tetikler</string>
     <string name="backup_data_new">Veri yedeklemesi oluşturma</string>
     <string name="pinentry_enter_pin">Devam etmek için Threema PIN\'inizi girin</string>
     <string name="pinentry_wrong_pin">PIN yanlış</string>
-    <string name="prefs_sum_security_pin">Threema Kullanıcı Arayüzüne erişimi kilitleme</string>
+    <string name="prefs_sum_security_pin">Erişimi kilitleyin</string>
     <string name="prefs_title_pin_switch">Uygulama koruması</string>
     <string name="prefs_title_pin_code">PIN belirleyin</string>
     <string name="prefs_pin_grace">Kilitlenme zamanı</string>
     <string name="prefs_sum_pin_grace">Ekran kilidinin etkinleşmesine kadar geçen süre</string>
     <string name="click_here_to_change_pin">PIN ayarlandı. Değiştirmek için buraya hafifçe vurun</string>
     <string name="set_pin_menu_title">Yeni PIN ayarla</string>
-    <string name="set_pin_summary_intro">Sayısal bir PIN (yalnızca rakamlar) ayarlayarak gizliliğinizi koruyun. Bu PIN, yetkisiz bir süre geçtikten sonra Threema\'nın kullanıcı arayüzüne erişimi kilitlemek veya özel sohbetleri korumak için kullanılabilir</string>
+    <string name="set_pin_summary_intro">Korumak için kullanılabilir Sayısal bir PIN (yalnızca rakamlar) ayarlayarak gizliliğinizi koruyun. Bu PIN, belirli bir sürenin ardından Threema\'nın kullanıcı arayüzüne erişimi kilitlemek veya özel sohbetleri korumak için kullanılabilir.</string>
     <string name="set_pin_again_summary">PIN kodunu tekrar girin</string>
     <string name="set_pin_hint">TOPLU İĞNE</string>
     <string name="title_addgroup">Yeni Grup</string>
@@ -504,7 +503,7 @@ anket oluşturun veya arkadaşlarınıza istediğiniz her şeyi sorun.</string>
     <string name="kick_user_from_group">Gruptan %1$s başladı</string>
     <string name="show_as_qrcode">QR kodu olarak göster</string>
     <string name="qr_code">QR kod</string>
-    <string name="really_leave_id_export">Henüz yapmadıysanız, kimlik dışa aktarma metin dizenizi veya ilgili QR kodunu güvenli bir yere kaydedin veya yazdırın. Threema ID\'niz yedek olmadan geri yüklenemez.</string>
+    <string name="really_leave_id_export">Henüz yapmadıysanız, kimlik dışa aktarma metin dizenizi veya ilgili QR kodunu güvenli bir yere kaydedin veya yazdırın. Kişisel Threema kimliği yedek olmadan geri alınamaz.</string>
     <string name="revocation_key_title">Kimlik İptali</string>
     <string name="revocation_key_not_set">İptal şifresi ayarlanmadı</string>
     <string name="revocation_key_set_at">%1$s şifre seti</string>
@@ -640,7 +639,7 @@ Yalnızca WiFi ağlarına dosya yüklemenizi ve indirmenizi öneririz .</string>
     <string name="new_wizard_hint_email">E-posta isteğe bağlı)</string>
     <string name="new_wizard_find_friends">Threema\'da arkadaşlarınızı bulun</string>
     <string name="new_wizard_sync_contacts_explain">Hangi arkadaşların Threema kullandığını görmek için açın.</string>
-    <string name="new_wizard_done_title">Bitti!\nHerşey doğru mu?</string>
+    <string name="new_wizard_done_title">Bitti!\nHer şey doğru mu?</string>
     <string name="new_wizard_linked_to">İle bağlantılı</string>
     <string name="new_wizard_need_internet">Benzersiz Threema ID\'nizi oluşturmak için sabit bir İnternet bağlantısı gerekir. Lütfen tekrar deneyin.</string>
     <string name="new_wizard_more_information">Daha fazla bilgi</string>
@@ -1214,5 +1213,13 @@ yöneticisiz kalır . Diğer üyeler yine de sohbet edebilecek, ancak artık de
     <string name="label_continue">Devam etmek</string>
     <string name="select_date">Bir tarih seçin</string>
     <string name="select_time">Bir zaman seçin</string>
-    <string name="recipients">Alıcılar: %s</string>
+    <string name="send_to">%s adresine gönder</string>
+    <string name="receipts_override_choice_send">Göndermek</string>
+    <string name="receipts_override_choice_dont_send">gönderme</string>
+    <string name="receipts_override_choice_default">Varsayılan ( %s )</string>
+    <string name="unable_to_determine_recording_length">Boş kayıt veya uzunluk belirlenemiyor</string>
+    <string name="prefs_header_receipts">Gelirler</string>
+    <string name="prefs_title_reset_receipts">Bireysel ayarları temizle</string>
+    <string name="prefs_sum_reset_receipts">Okundu bilgileri ve yazma göstergesi için kişiye özel ayarları varsayılana sıfırlayın</string>
+    <string name="reset_successful">Varsayılanlara başarıyla sıfırlandı</string>
 </resources>

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

@@ -1061,8 +1061,8 @@ Threema ID。您将不会出现在朋友的联系人列表中。您确定要
     <string name="two_years">2年</string>
     <string name="invalid_backup_path">无效的备份路径</string>
     <string name="backup_data_no_permission">无法写入此目录。请选择另一个。</string>
-    <string name="prefs_sum_show_unread_badge">在消息图标旁边显示徽章,表示未读消息的数量</string>
-    <string name="prefs_title_show_unread_badge">未读邮件徽章</string>
+    <string name="prefs_sum_show_unread_badge">在底部导航栏显示未读消息提示</string>
+    <string name="prefs_title_show_unread_badge">未读消息提示</string>
     <string name="pinning_not_trusted">证书固定失败。请检查是否在设备的凭据存储中安装并激活了 “委托根证书颁发机构-G2”。</string>
     <string name="pinning_failed">证书固定失败。可能发生中间人攻击。如果您安装了广告拦截器、内容过滤器或防火墙应用程序,例如 AdGuard,请禁用后重开 Threema。</string>
     <string name="open_myid_popup">打开快速访问弹出窗口</string>
@@ -1222,11 +1222,19 @@ Threema ID。您将不会出现在朋友的联系人列表中。您确定要
     <string name="download_failed">下载失败。错误代码:%d</string>
     <string name="edit_answer">编辑答案</string>
     <string name="share_media">与其他应用分享...</string>
-    <string name="miui_battery_optimization">请为 %1$s 禁用 “电池优化” 以便 %1$s 顺利运行。由于小米不允许第三方应用直接打开设置菜单,因此您必须手动打开手机的相关设置菜单。如果您在禁用电池优化方面需要帮助,请联系小米支持。</string>
+    <string name="miui_battery_optimization">请为 %s 禁用 “电池优化” 以便 %s 顺利运行。由于小米不允许第三方应用直接打开设置菜单,因此您必须手动打开手机的相关设置菜单。如果您在禁用电池优化方面需要帮助,请联系小米支持。</string>
     <string name="forward_captions">包含描述</string>
     <string name="importing_files_failed">无法导入备份文件,请确保内部存储有足够的可用空间。</string>
     <string name="label_continue">继续</string>
     <string name="select_date">选择日期</string>
     <string name="select_time">选择时间</string>
-    <string name="recipients">收件人:%s</string>
+    <string name="send_to">发送到 %s</string>
+    <string name="receipts_override_choice_send">发送</string>
+    <string name="receipts_override_choice_dont_send">不要发送</string>
+    <string name="receipts_override_choice_default">默认(%s)</string>
+    <string name="unable_to_determine_recording_length">录音为空白或无法确定录音长度</string>
+    <string name="prefs_header_receipts">消息回执和输入状态</string>
+    <string name="prefs_title_reset_receipts">清除个别设置</string>
+    <string name="prefs_sum_reset_receipts">将特定联系人的已读回执和输入状态设置重置为默认值</string>
+    <string name="reset_successful">成功重置为默认值</string>
 </resources>

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

@@ -11,7 +11,7 @@
     <string name="title_enter_id">輸入ID</string>
     <string name="title_invite_friend">邀請好友</string>
     <string name="invite_via">透過...邀請好友</string>
-    <string name="invite_email_body">你好,\n\n我使用%1$s,這是保護使用者隱私的安全即時通訊工具。\n\n我的Threema ID:https://threema.id/%2$s\n\n讓我們透過%1$s進行通訊!\n\n乾杯\n</string>
+    <string name="invite_email_body">你好,\n\n我使用%1$s,這是保護使用者隱私的安全即時通訊工具。\n\n我的Threema ID:https://threema.id/%2$s\n\n讓我們透過%1$s進行通訊!\n\n乾杯\n</string>
     <string name="invite_sms_body">嘿!讓我們使用%1$s以安全和符合隱私的方式進行通訊!我的 Threema ID:https://threema.id/%2$s</string>
     <string name="invite_email_subject">Threema,保護隱私的安全即時通訊工具。</string>
     <string name="enter_id_hint">輸入 Threema ID</string>
@@ -1053,8 +1053,8 @@ Threema 支援的所有表情符號。</string>
     <string name="two_years">2年</string>
     <string name="invalid_backup_path">無效的備份路徑</string>
     <string name="backup_data_no_permission">無法寫入此目錄。請選擇另一個。</string>
-    <string name="prefs_sum_show_unread_badge">在訊息圖示旁邊顯示徽章,表示未讀訊息的數量</string>
-    <string name="prefs_title_show_unread_badge">未讀訊息徽章</string>
+    <string name="prefs_sum_show_unread_badge">在導覽列顯示未讀訊息提示</string>
+    <string name="prefs_title_show_unread_badge">未讀訊息提示</string>
     <string name="pinning_not_trusted">憑證固定失敗。請檢查裝置認證儲存中是否安裝並啟動了「委託根認證機構 - G2」。</string>
     <string name="pinning_failed">憑證固定失敗。可能發生了中間人攻擊。如果您安裝了廣告攔截器、內容過濾器或防火牆應用程式,例如 AdGuard,請停用後重啟 Threema。</string>
     <string name="open_myid_popup">開啟快速存取彈出式視窗</string>
@@ -1213,11 +1213,19 @@ Threema 支援的所有表情符號。</string>
     <string name="download_failed">下載失敗。錯誤代碼:%d</string>
     <string name="edit_answer">編輯答案</string>
     <string name="share_media">與其他應用程式分享...</string>
-    <string name="miui_battery_optimization">請為 %1$s 停用「電池最佳化」以便 %1$s 順利執行。由於小米不允許第三方應用程式直接開啟設定畫面,因此您必須手動開啟電話的相關設定畫面。如果您在停用電池最佳化方面需要協助,請聯絡小米支援。</string>
+    <string name="miui_battery_optimization">請為 %s 停用「電池最佳化」以便 %s 順利執行。由於小米不允許第三方應用程式直接開啟設定畫面,因此您必須手動開啟電話的相關設定畫面。如果您在停用電池最佳化方面需要協助,請聯絡小米支援。</string>
     <string name="forward_captions">包括描述</string>
     <string name="importing_files_failed">無法匯入備份檔案,請確保內部儲存有足夠的可用空間。</string>
     <string name="label_continue">繼續</string>
     <string name="select_date">選擇日期</string>
     <string name="select_time">選擇時間</string>
-    <string name="recipients">收件者:%s</string>
+    <string name="send_to">傳送至 %s</string>
+    <string name="receipts_override_choice_send">傳送</string>
+    <string name="receipts_override_choice_dont_send">不要傳送</string>
+    <string name="receipts_override_choice_default">預設(%s)</string>
+    <string name="unable_to_determine_recording_length">錄音為空白或無法確定錄音長度</string>
+    <string name="prefs_header_receipts">訊息標記和打字狀態</string>
+    <string name="prefs_title_reset_receipts">清除個別設定</string>
+    <string name="prefs_sum_reset_receipts">將特定聯絡人的已讀標記和打字狀態設定重設為預設值</string>
+    <string name="reset_successful">成功重設為預設值</string>
 </resources>

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

@@ -326,4 +326,10 @@
 		<item>1</item>
 		<item>2</item>
 	</string-array>
+
+	<string-array name="receipts_override_choices">
+		<item></item>
+		<item>@string/receipts_override_choice_send</item>
+		<item>@string/receipts_override_choice_dont_send</item>
+	</string-array>
 </resources>

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

@@ -310,5 +310,6 @@
 
 	<!-- vertical offset for badges in home nav bar -->
 	<dimen name="bottom_nav_badge_offset_vertical">2dp</dimen>
+	<dimen name="header_contact_section_work_height">40dp</dimen>
 
 </resources>

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

@@ -3,7 +3,7 @@
 	<string name="title_section2">Contacts</string>
 	<string name="title_section1">Chats</string>
 	<string name="title_compose_message">Start chat</string>
-	<string name="title_choose_recipient">Select Recipient</string>
+	<string name="title_choose_recipient">Select Recipients</string>
 	<string name="title_keyfingerprint">Key Fingerprint</string>
 	<string name="title_mythreemaid">My Threema ID</string>
 	<string name="title_threemaid">Threema ID</string>
@@ -1239,7 +1239,14 @@
 	<string name="label_continue">Continue</string>
 	<string name="select_date">Select a date</string>
 	<string name="select_time">Select a time</string>
-	<string name="recipients">Recipients: %s</string>
 	<string name="send_to">Send to %s</string>
+	<string name="receipts_override_choice_send">Send</string>
+	<string name="receipts_override_choice_dont_send">Don\'t send</string>
+	<string name="receipts_override_choice_default">Default (%s)</string>
+	<string name="unable_to_determine_recording_length">Empty recording or unable to determine length</string>
+	<string name="prefs_header_receipts">Receipts</string>
+	<string name="prefs_title_reset_receipts">Clear individual settings</string>
+	<string name="prefs_sum_reset_receipts">Reset contact-specific settings for read receipts and typing indicator to default</string>
+	<string name="reset_successful">Successfully reset to defaults</string>
 </resources>
 

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

@@ -627,6 +627,7 @@
 	</style>
 
 	<style name="Threema.MaterialButton.Fatal" parent="@style/Widget.MaterialComponents.Button">
+		<item name="android:textColor">@android:color/white</item>
 		<item name="android:backgroundTint">@color/material_red</item>
 	</style>
 
@@ -661,6 +662,14 @@
 		<item name="android:textAppearance">@style/Threema.TextAppearance.Chip</item>
 	</style>
 
+	<style name="Threema.Chip.Action" parent="@style/Widget.MaterialComponents.Chip.Action">
+		<item name="android:textAppearance">@style/Threema.TextAppearance.Chip</item>
+		<item name="android:textColor">@android:color/white</item>
+		<item name="chipBackgroundColor">?attr/colorAccent</item>
+		<item name="chipCornerRadius">18dp</item>
+		<item name="chipMinHeight">36dp</item>
+	</style>
+
 	<style name="Threema.Chip.Outline.Action" parent="@style/Widget.MaterialComponents.Chip.Action">
 		<item name="android:textAppearance">@style/Threema.TextAppearance.Chip</item>
 		<item name="android:textColor">?attr/colorAccent</item>

+ 6 - 0
app/src/main/res/xml/network_security_config.xml

@@ -5,6 +5,12 @@
   -->
 <network-security-config>
 	<!-- Official Android N API -->
+	<base-config cleartextTrafficPermitted="false">
+		<trust-anchors>
+			<certificates src="system" />
+			<certificates src="user" />
+		</trust-anchors>
+	</base-config>
 	<domain-config cleartextTrafficPermitted="false">
 		<domain includeSubdomains="true">threema.ch</domain>
 		<pin-set>

+ 9 - 3
app/src/main/res/xml/preference_privacy.xml

@@ -30,17 +30,23 @@
 
 	<PreferenceCategory
 		android:key="pref_key_chat"
-		android:title="@string/prefs_header_chat">
+		android:title="@string/prefs_header_receipts">
 
 		<CheckBoxPreference
 			android:defaultValue="true"
 			android:key="@string/preferences__read_receipts"
-			android:title="@string/prefs_title_read_receipts"/>
+			android:title="@string/prefs_title_read_receipts" />
 
 		<CheckBoxPreference
 			android:defaultValue="true"
 			android:key="@string/preferences__typing_indicator"
-			android:title="@string/prefs_title_typing_indicator"/>
+			android:title="@string/prefs_title_typing_indicator" />
+
+		<Preference
+			android:enabled="true"
+			android:key="pref_reset_receipts"
+			android:summary="@string/prefs_sum_reset_receipts"
+			android:title="@string/prefs_title_reset_receipts"/>
 
 	</PreferenceCategory>
 

+ 38 - 0
app/src/none/AndroidManifest.xml

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:installLocation="internalOnly"
+	android:testOnly="false">
+
+	<application tools:ignore="GoogleAppIndexingWarning">
+
+		<meta-data
+			android:name="com.google.android.gms.version"
+			android:value="@integer/google_play_services_version"/>
+		<meta-data
+			android:name="com.google.android.gms.car.application"
+			android:resource="@xml/automotive_app_desc"/>
+		<meta-data
+			android:name="firebase_analytics_collection_deactivated"
+			android:value="true"/>
+		<meta-data
+			android:name="google_analytics_adid_collection_enabled"
+			android:value="false"/>
+		<meta-data
+			android:name="google_analytics_ssaid_collection_enabled"
+			android:value="false" />
+		<meta-data
+			android:name="firebase_messaging_auto_init_enabled"
+			android:value="false"/>
+
+		<service
+			android:name="ch.threema.app.push.PushService"
+			android:exported="false">
+			<intent-filter>
+				<action android:name="com.google.firebase.MESSAGING_EVENT" />
+			</intent-filter>
+		</service>
+
+	</application>
+
+</manifest>

+ 39 - 0
app/src/red/res/layout/header_contact_section_work.xml

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (c) 2021 Threema GmbH
+  ~ All rights reserved.
+  -->
+<androidx.constraintlayout.widget.ConstraintLayout
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:app="http://schemas.android.com/apk/res-auto"
+	android:layout_width="match_parent"
+	android:layout_height="@dimen/header_contact_section_work_height"
+	android:background="?attr/background_secondary">
+
+	<com.google.android.material.tabs.TabLayout
+		android:id="@+id/work_contacts_tab_layout"
+		android:layout_width="match_parent"
+		android:layout_height="36dp"
+		android:elevation="4dp"
+		app:layout_constraintLeft_toLeftOf="parent"
+		app:layout_constraintRight_toRightOf="parent"
+		app:layout_constraintTop_toTopOf="parent"
+		app:tabGravity="fill"
+		app:tabMode="fixed">
+
+		<com.google.android.material.tabs.TabItem
+			android:id="@+id/tab_all_contacts"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:icon="@drawable/ic_person_outline"
+			android:contentDescription="Alle Kontakte" />
+
+		<com.google.android.material.tabs.TabItem
+			android:id="@+id/tab_work_contacts"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:icon="@drawable/ic_work_outline"
+			android:contentDescription="Work-Kontakte" />
+
+	</com.google.android.material.tabs.TabLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 0 - 10
app/src/red/res/xml/backup_content.xml

@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<full-backup-content>
-	<include domain="file" path="backup/threema.db"/>
-	<include domain="file" path="backup/ch.threema.app.work_preferences.xml"/>
-	<include domain="file" path="backup/keybackup.bin"/>
-	<!-- bug in backup agent: you cannot specify multiple includes for external domain -->
-	<include domain="external"/>
-	<exclude domain="external" path="data"/>
-	<exclude domain="external" path="tmp"/>
-</full-backup-content>

+ 13 - 7
app/src/red/res/xml/preference_appearance.xml

@@ -16,6 +16,14 @@
 			android:entryValues="@array/list_theme_values"
 			android:defaultValue="1"/>
 
+		<DropDownPreference
+			android:key="@string/preferences__language_override"
+			android:title="@string/prefs_language_override"
+			android:icon="@drawable/ic_language_outline"
+			android:entries="@array/list_language_override"
+			android:entryValues="@array/list_language_override_values"
+			android:defaultValue=""/>
+
 		<DropDownPreference
 			android:key="@string/preferences__emoji_style"
 			android:title="@string/prefs_emoji_style"
@@ -43,13 +51,11 @@
 			android:summaryOn="@string/prefs_sum_receive_profilepics_on"
 			android:title="@string/prefs_title_receive_profilepics"/>
 
-	<DropDownPreference
-			android:key="@string/preferences__language_override"
-			android:title="@string/prefs_language_override"
-			android:icon="@drawable/ic_language_outline"
-			android:entries="@array/list_language_override"
-			android:entryValues="@array/list_language_override_values"
-			android:defaultValue=""/>
+		<CheckBoxPreference
+			android:defaultValue="true"
+			android:key="@string/preferences__show_unread_badge"
+			android:summary="@string/prefs_sum_show_unread_badge"
+			android:title="@string/prefs_title_show_unread_badge"/>
 
 	</PreferenceCategory>
 

+ 39 - 0
app/src/store_google_work/res/layout/header_contact_section_work.xml

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (c) 2021 Threema GmbH
+  ~ All rights reserved.
+  -->
+<androidx.constraintlayout.widget.ConstraintLayout
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:app="http://schemas.android.com/apk/res-auto"
+	android:layout_width="match_parent"
+	android:layout_height="@dimen/header_contact_section_work_height"
+	android:background="?attr/background_secondary">
+
+	<com.google.android.material.tabs.TabLayout
+		android:id="@+id/work_contacts_tab_layout"
+		android:layout_width="match_parent"
+		android:layout_height="36dp"
+		android:elevation="4dp"
+		app:layout_constraintLeft_toLeftOf="parent"
+		app:layout_constraintRight_toRightOf="parent"
+		app:layout_constraintTop_toTopOf="parent"
+		app:tabGravity="fill"
+		app:tabMode="fixed">
+
+		<com.google.android.material.tabs.TabItem
+			android:id="@+id/tab_all_contacts"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:icon="@drawable/ic_person_outline"
+			android:contentDescription="Alle Kontakte" />
+
+		<com.google.android.material.tabs.TabItem
+			android:id="@+id/tab_work_contacts"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:icon="@drawable/ic_work_outline"
+			android:contentDescription="Work-Kontakte" />
+
+	</com.google.android.material.tabs.TabLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 29 - 0
app/src/store_google_work/res/xml/app_shortcuts.xml

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
+	<shortcut
+		android:enabled="true"
+		android:icon="@mipmap/ic_shortcut_compose"
+		android:shortcutDisabledMessage="@string/title_compose_message"
+		android:shortcutId="compose"
+		android:shortcutLongLabel="@string/title_compose_message"
+		android:shortcutShortLabel="@string/title_compose_message">
+		<intent
+			android:action="android.intent.action.VIEW"
+			android:targetClass="ch.threema.app.activities.RecipientListBaseActivity"
+			android:targetPackage="ch.threema.app.work"/>
+		<categories android:name="android.shortcut.conversation"/>
+	</shortcut>
+	<shortcut
+		android:enabled="true"
+		android:icon="@mipmap/ic_shortcut_qr"
+		android:shortcutDisabledMessage="@string/qr_code"
+		android:shortcutId="qrcode"
+		android:shortcutLongLabel="@string/qr_code"
+		android:shortcutShortLabel="@string/qr_code">
+		<intent
+			android:action="android.intent.action.VIEW"
+			android:targetClass="ch.threema.app.activities.QRCodeZoomActivity"
+			android:targetPackage="ch.threema.app.work"/>
+		<categories android:name="android.shortcut.conversation"/>
+	</shortcut>
+</shortcuts>

+ 0 - 10
app/src/store_google_work/res/xml/backup_content.xml

@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<full-backup-content>
-	<include domain="file" path="backup/threema.db"/>
-	<include domain="file" path="backup/ch.threema.app.work_preferences.xml"/>
-	<include domain="file" path="backup/keybackup.bin"/>
-	<!-- bug in backup agent: you cannot specify multiple includes for external domain -->
-	<include domain="external"/>
-	<exclude domain="external" path="data"/>
-	<exclude domain="external" path="tmp"/>
-</full-backup-content>

+ 13 - 7
app/src/store_google_work/res/xml/preference_appearance.xml

@@ -16,6 +16,14 @@
 			android:entryValues="@array/list_theme_values"
 			android:defaultValue="1"/>
 
+		<DropDownPreference
+			android:key="@string/preferences__language_override"
+			android:title="@string/prefs_language_override"
+			android:icon="@drawable/ic_language_outline"
+			android:entries="@array/list_language_override"
+			android:entryValues="@array/list_language_override_values"
+			android:defaultValue=""/>
+
 		<DropDownPreference
 			android:key="@string/preferences__emoji_style"
 			android:title="@string/prefs_emoji_style"
@@ -44,13 +52,11 @@
 			android:icon="@drawable/ic_outline_account_circle_24"
 			android:title="@string/prefs_title_receive_profilepics"/>
 
-	<DropDownPreference
-			android:key="@string/preferences__language_override"
-			android:title="@string/prefs_language_override"
-			android:icon="@drawable/ic_language_outline"
-			android:entries="@array/list_language_override"
-			android:entryValues="@array/list_language_override_values"
-			android:defaultValue=""/>
+		<CheckBoxPreference
+			android:defaultValue="true"
+			android:key="@string/preferences__show_unread_badge"
+			android:summary="@string/prefs_sum_show_unread_badge"
+			android:title="@string/prefs_title_show_unread_badge"/>
 
 	</PreferenceCategory>
 

+ 1 - 17
app/src/test/java/ch/threema/client/Helpers.java

@@ -22,9 +22,6 @@
 package ch.threema.client;
 
 import ch.threema.base.Contact;
-import ch.threema.client.*;
-
-import java.util.Collection;
 
 public class Helpers {
 
@@ -42,27 +39,14 @@ public class Helpers {
 				return null;
 			}
 
-			@Override
-			public Collection<Contact> getAllContacts() {
-				return null;
-			}
-
 			@Override
 			public void addContact(Contact contact) { }
 
 			@Override
-			public void hideContact(Contact contact, boolean hide) {
-
-			}
+			public void hideContact(Contact contact, boolean hide) { }
 
 			@Override
 			public void removeContact(Contact contact) { }
-
-			@Override
-			public void addContactStoreObserver(ContactStoreObserver observer) { }
-
-			@Override
-			public void removeContactStoreObserver(ContactStoreObserver observer) { }
 		};
 	}
 

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác