Kaynağa Gözat

Version 4.72

Threema 3 yıl önce
ebeveyn
işleme
d2f1941eb6
100 değiştirilmiş dosya ile 4213 ekleme ve 2999 silme
  1. BIN
      app/assets/emojis/activity-0.png
  2. BIN
      app/assets/emojis/activity-1.png
  3. BIN
      app/assets/emojis/flags-0.png
  4. BIN
      app/assets/emojis/food-0.png
  5. BIN
      app/assets/emojis/nature-0.png
  6. BIN
      app/assets/emojis/objects-0.png
  7. BIN
      app/assets/emojis/people-0.png
  8. BIN
      app/assets/emojis/people-1.png
  9. BIN
      app/assets/emojis/people-2.png
  10. BIN
      app/assets/emojis/people-3.png
  11. BIN
      app/assets/emojis/people-4.png
  12. BIN
      app/assets/emojis/people-5.png
  13. BIN
      app/assets/emojis/symbols-0.png
  14. BIN
      app/assets/emojis/travel-0.png
  15. 16 10
      app/build.gradle
  16. BIN
      app/libs/agconnect-apms-plugin-1.5.2.302.jar
  17. BIN
      app/libs/agconnect-core-1.4.0.300.aar
  18. BIN
      app/libs/agconnect-core-1.5.0.300.aar
  19. BIN
      app/libs/agconnect-crash-symbol-lib-1.6.0.300.jar
  20. BIN
      app/libs/agcp-1.4.2.300.jar
  21. BIN
      app/libs/agcp-1.6.0.300.jar
  22. 4 3
      app/src/androidTest/java/ch/threema/logging/backend/DebugLogFileBackendTest.java
  23. 5 3
      app/src/foss_based/java/ch/threema/app/activities/VoiceActionActivity.java
  24. 15 3
      app/src/foss_based/java/ch/threema/app/services/VoiceActionService.java
  25. 4 2
      app/src/hms_services_based/java/ch/threema/app/activities/VoiceActionActivity.java
  26. 1 0
      app/src/hms_services_based/java/ch/threema/app/licensing/StoreLicenseCheck.java
  27. 1 0
      app/src/hms_services_based/java/ch/threema/app/push/PushRegistrationWorker.java
  28. 1 0
      app/src/hms_services_based/java/ch/threema/app/push/PushService.java
  29. 13 2
      app/src/hms_services_based/java/ch/threema/app/services/VoiceActionService.java
  30. 0 6
      app/src/main/AndroidManifest.xml
  31. 1 1
      app/src/main/java/ch/threema/app/ThreemaApplication.java
  32. 8 1
      app/src/main/java/ch/threema/app/activities/RecipientListBaseActivity.java
  33. 7 3
      app/src/main/java/ch/threema/app/adapters/ContactListAdapter.java
  34. 2 1
      app/src/main/java/ch/threema/app/adapters/UserListAdapter.java
  35. 1 1
      app/src/main/java/ch/threema/app/adapters/decorators/LocationChatAdapterDecorator.java
  36. 3 3
      app/src/main/java/ch/threema/app/backuprestore/csv/RestoreService.java
  37. 52 22
      app/src/main/java/ch/threema/app/camera/CameraFragment.kt
  38. 6 18
      app/src/main/java/ch/threema/app/camera/CameraFragmentViewModel.kt
  39. 1 1
      app/src/main/java/ch/threema/app/emojis/EmojiCategory.java
  40. 3096 2490
      app/src/main/java/ch/threema/app/emojis/EmojiParser.java
  41. 215 125
      app/src/main/java/ch/threema/app/emojis/EmojiSpritemap.java
  42. 1 1
      app/src/main/java/ch/threema/app/emojis/SpriteCoordinates.java
  43. 19 14
      app/src/main/java/ch/threema/app/fragments/ComposeMessageFragment.java
  44. 5 0
      app/src/main/java/ch/threema/app/fragments/DistributionListFragment.java
  45. 1 3
      app/src/main/java/ch/threema/app/fragments/MessageSectionFragment.java
  46. 95 3
      app/src/main/java/ch/threema/app/fragments/RecipientListFragment.java
  47. 1 1
      app/src/main/java/ch/threema/app/fragments/UserListFragment.java
  48. 1 1
      app/src/main/java/ch/threema/app/fragments/WorkUserListFragment.java
  49. 7 1
      app/src/main/java/ch/threema/app/globalsearch/GlobalSearchActivity.java
  50. 1 1
      app/src/main/java/ch/threema/app/mediaattacher/MediaAttachActivity.java
  51. 1 1
      app/src/main/java/ch/threema/app/messagereceiver/DistributionListContactMessageReceiver.java
  52. 1 1
      app/src/main/java/ch/threema/app/notifications/NotificationBuilderWrapper.java
  53. 7 3
      app/src/main/java/ch/threema/app/preference/SettingsAboutFragment.kt
  54. 4 7
      app/src/main/java/ch/threema/app/preference/SettingsActivity.kt
  55. 4 3
      app/src/main/java/ch/threema/app/preference/SettingsAppearanceFragment.kt
  56. 2 0
      app/src/main/java/ch/threema/app/preference/SettingsCallsFragment.kt
  57. 1 0
      app/src/main/java/ch/threema/app/preference/SettingsChatFragment.kt
  58. 3 2
      app/src/main/java/ch/threema/app/preference/SettingsDeveloperFragment.java
  59. 1 1
      app/src/main/java/ch/threema/app/preference/SettingsFragment.kt
  60. 1 1
      app/src/main/java/ch/threema/app/preference/SettingsMediaDummyActivity.java
  61. 1 1
      app/src/main/java/ch/threema/app/preference/SettingsMediaFragment.kt
  62. 0 1
      app/src/main/java/ch/threema/app/preference/SettingsNotificationsDummyActivity.java
  63. 8 7
      app/src/main/java/ch/threema/app/preference/SettingsNotificationsFragment.java
  64. 24 7
      app/src/main/java/ch/threema/app/preference/SettingsPrivacyFragment.kt
  65. 6 10
      app/src/main/java/ch/threema/app/preference/SettingsRateFragment.java
  66. 21 32
      app/src/main/java/ch/threema/app/preference/SettingsSecurityFragment.java
  67. 39 24
      app/src/main/java/ch/threema/app/preference/SettingsTroubleshootingFragment.java
  68. 27 15
      app/src/main/java/ch/threema/app/preference/ThreemaPreferenceFragment.kt
  69. 4 2
      app/src/main/java/ch/threema/app/processors/MessageProcessor.java
  70. 4 2
      app/src/main/java/ch/threema/app/services/ContactServiceImpl.java
  71. 1 1
      app/src/main/java/ch/threema/app/services/ConversationServiceImpl.java
  72. 5 1
      app/src/main/java/ch/threema/app/services/DistributionListService.java
  73. 8 2
      app/src/main/java/ch/threema/app/services/DistributionListServiceImpl.java
  74. 8 0
      app/src/main/java/ch/threema/app/services/FileService.java
  75. 6 1
      app/src/main/java/ch/threema/app/services/FileServiceImpl.java
  76. 1 0
      app/src/main/java/ch/threema/app/services/NotificationService.java
  77. 33 16
      app/src/main/java/ch/threema/app/services/NotificationServiceImpl.java
  78. 6 4
      app/src/main/java/ch/threema/app/services/PreferenceService.java
  79. 26 17
      app/src/main/java/ch/threema/app/services/PreferenceServiceImpl.java
  80. 57 0
      app/src/main/java/ch/threema/app/services/systemupdate/SystemUpdateToVersion71.java
  81. 57 41
      app/src/main/java/ch/threema/app/stores/DatabaseContactStore.java
  82. 10 4
      app/src/main/java/ch/threema/app/stores/PreferenceStore.java
  83. 4 1
      app/src/main/java/ch/threema/app/stores/PreferenceStoreInterface.java
  84. 8 1
      app/src/main/java/ch/threema/app/stores/PreferenceStoreInterfaceDevNullImpl.java
  85. 5 0
      app/src/main/java/ch/threema/app/threemasafe/ThreemaSafeServiceImpl.java
  86. 9 6
      app/src/main/java/ch/threema/app/utils/ConfigUtils.java
  87. 36 5
      app/src/main/java/ch/threema/app/utils/DNDUtil.java
  88. 30 0
      app/src/main/java/ch/threema/app/utils/ShareUtil.java
  89. 7 0
      app/src/main/java/ch/threema/app/utils/ShortcutUtil.java
  90. 3 1
      app/src/main/java/ch/threema/app/voicemessage/VoiceRecorderActivity.java
  91. 10 6
      app/src/main/java/ch/threema/app/voip/VoipBluetoothManager.java
  92. 1 1
      app/src/main/java/ch/threema/app/voip/activities/CallActivity.java
  93. 74 13
      app/src/main/java/ch/threema/app/voip/services/CallRejectService.java
  94. 21 22
      app/src/main/java/ch/threema/app/voip/services/VoipStateService.java
  95. 3 2
      app/src/main/java/ch/threema/logging/backend/DebugLogFileBackend.java
  96. 3 2
      app/src/main/java/ch/threema/logging/backend/LogcatBackend.java
  97. 5 1
      app/src/main/java/ch/threema/storage/DatabaseServiceNew.java
  98. 15 5
      app/src/main/java/ch/threema/storage/factories/DistributionListModelFactory.java
  99. 14 3
      app/src/main/java/ch/threema/storage/models/DistributionListModel.java
  100. 5 3
      app/src/main/res/layout/activity_unlock_masterkey.xml

BIN
app/assets/emojis/activity-0.png


BIN
app/assets/emojis/activity-1.png


BIN
app/assets/emojis/flags-0.png


BIN
app/assets/emojis/food-0.png


BIN
app/assets/emojis/nature-0.png


BIN
app/assets/emojis/objects-0.png


BIN
app/assets/emojis/people-0.png


BIN
app/assets/emojis/people-1.png


BIN
app/assets/emojis/people-2.png


BIN
app/assets/emojis/people-3.png


BIN
app/assets/emojis/people-4.png


BIN
app/assets/emojis/people-5.png


BIN
app/assets/emojis/symbols-0.png


BIN
app/assets/emojis/travel-0.png


+ 16 - 10
app/build.gradle

@@ -12,7 +12,7 @@ if (getGradle().getStartParameter().getTaskRequests().toString().contains("Hms")
 }
 
 // version codes
-def app_version = "4.71"
+def app_version = "4.72"
 def beta_suffix = "" // with leading dash
 
 /**
@@ -92,7 +92,7 @@ android {
         vectorDrawables.useSupportLibrary = true
         applicationId "ch.threema.app"
         testApplicationId 'ch.threema.app.test'
-        versionCode 736
+        versionCode 738
         versionName "${app_version}${beta_suffix}"
         resValue "string", "app_name", "Threema"
         // package name used for sync adapter - needs to match mime types below
@@ -125,6 +125,7 @@ android {
         buildConfigField "String", "SAFE_SERVER_URL", "\"https://safe-%h.threema.ch/\""
         buildConfigField "String", "WEB_SERVER_URL", "\"https://web.threema.ch/\""
         buildConfigField "String", "ONPREM_ID_PREFIX", "\"O\""
+        buildConfigField "String", "LOG_TAG", "\"3ma\""
 
         buildConfigField "String[]", "ONPREM_CONFIG_TRUSTED_PUBLIC_KEYS", "null"
         buildConfigField "boolean", "SEND_CONSUMED_DELIVERY_RECEIPTS", "false"
@@ -191,6 +192,7 @@ android {
             buildConfigField "String", "MEDIA_PATH", "\"ThreemaWork\""
             buildConfigField "String", "WORK_SERVER_URL", "\"https://apip-work.threema.ch/\""
             buildConfigField "String", "WORK_SERVER_IPV6_URL", "\"https://ds-apip-work.threema.ch/\""
+            buildConfigField "String", "LOG_TAG", "\"3mawrk\""
 
             // config fields for action URLs / deep links
             buildConfigField "String", "uriScheme", "\"threemawork\""
@@ -236,6 +238,7 @@ android {
             buildConfigField "String", "WORK_SERVER_URL", "\"https://apip-work.test.threema.ch/\""
             buildConfigField "String", "WORK_SERVER_IPV6_URL", "\"https://ds-apip-work.test.threema.ch/\""
             buildConfigField "String", "AVATAR_FETCH_URL", "\"https://avatar.test.threema.ch/\""
+            buildConfigField "String", "LOG_TAG", "\"3mawrk\""
 
             // config fields for action URLs / deep links
             buildConfigField "String", "uriScheme", "\"threemawork\""
@@ -272,6 +275,7 @@ android {
             buildConfigField "String", "BLOB_SERVER_UPLOAD_URL", "null"
             buildConfigField "String", "BLOB_SERVER_UPLOAD_IPV6_URL", "null"
             buildConfigField "String[]", "ONPREM_CONFIG_TRUSTED_PUBLIC_KEYS", "new String[] {\"ek1qBp4DyRmLL9J5sCmsKSfwbsiGNB4veDAODjkwe/k=\", \"Hrk8aCjwKkXySubI7CZ3y9Sx+oToEHjNkGw98WSRneU=\", \"5pEn1T/5bhecNWrp9NgUQweRfgVtu/I8gRb3VxGP7k4=\"}"
+            buildConfigField "String", "LOG_TAG", "\"3maop\""
 
             // config fields for action URLs / deep links
             buildConfigField "String", "uriScheme", "\"threemaonprem\""
@@ -303,6 +307,7 @@ android {
             buildConfigField "String", "WORK_SERVER_URL", "\"https://apip-work.test.threema.ch/\""
             buildConfigField "String", "WORK_SERVER_IPV6_URL", "\"https://ds-apip-work.test.threema.ch/\""
             buildConfigField "String", "AVATAR_FETCH_URL", "\"https://avatar.test.threema.ch/\""
+            buildConfigField "String", "LOG_TAG", "\"3mared\""
 
             buildConfigField "boolean", "SEND_CONSUMED_DELIVERY_RECEIPTS", "true"
 
@@ -331,6 +336,7 @@ android {
             buildConfigField "String", "MEDIA_PATH", "\"ThreemaWork\""
             buildConfigField "String", "WORK_SERVER_URL", "\"https://apip-work.threema.ch/\""
             buildConfigField "String", "WORK_SERVER_IPV6_URL", "\"https://ds-apip-work.threema.ch/\""
+            buildConfigField "String", "LOG_TAG", "\"3mawrk\""
 
             // config fields for action URLs / deep links
             buildConfigField "String", "uriScheme", "\"threemawork\""
@@ -688,9 +694,9 @@ dependencies {
     implementation 'androidx.activity:activity-ktx:1.4.0'
     implementation 'androidx.sqlite:sqlite:2.1.0'
     implementation "androidx.concurrent:concurrent-futures:1.1.0"
-    implementation "androidx.camera:camera-camera2:1.1.0-beta02"
-    implementation "androidx.camera:camera-lifecycle:1.1.0-beta02"
-    implementation "androidx.camera:camera-view:1.1.0-beta02"
+    implementation "androidx.camera:camera-camera2:1.1.0-beta03"
+    implementation "androidx.camera:camera-lifecycle:1.1.0-beta03"
+    implementation "androidx.camera:camera-view:1.1.0-beta03"
     implementation 'androidx.multidex:multidex:2.0.1'
     implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
     implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
@@ -727,8 +733,8 @@ dependencies {
     }
 
     // Glide components
-    implementation 'com.github.bumptech.glide:glide:4.12.0'
-    annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
+    implementation 'com.github.bumptech.glide:glide:4.13.1'
+    annotationProcessor 'com.github.bumptech.glide:compiler:4.13.1'
 
     // kotlin
     implementation "androidx.core:core-ktx:1.7.0"
@@ -821,7 +827,7 @@ dependencies {
     // Huawei related libraries (only for hms* build variants)
     def huaweiDependencies = [
         // HMS push
-        'com.huawei.hms:push:5.0.4.302': [
+        'com.huawei.hms:push:6.3.0.304': [
             // Exclude agconnect dependency, we'll replace it with the vendored version below
             [group: 'com.huawei.agconnect'],
         ],
@@ -832,8 +838,8 @@ dependencies {
         hmsImplementation(dependency) { excludes.each { exclude it } }
         hms_workImplementation(dependency) { excludes.each { exclude it } }
     }
-    hmsImplementation(name: 'agconnect-core-1.4.0.300', ext: 'aar')
-    hms_workImplementation(name: 'agconnect-core-1.4.0.300', ext: 'aar')
+    hmsImplementation(name: 'agconnect-core-1.5.0.300', ext: 'aar')
+    hms_workImplementation(name: 'agconnect-core-1.5.0.300', ext: 'aar')
 }
 
 sonarqube {

BIN
app/libs/agconnect-apms-plugin-1.5.2.302.jar


BIN
app/libs/agconnect-core-1.4.0.300.aar


BIN
app/libs/agconnect-core-1.5.0.300.aar


BIN
app/libs/agconnect-crash-symbol-lib-1.4.2.300.jar → app/libs/agconnect-crash-symbol-lib-1.6.0.300.jar


BIN
app/libs/agcp-1.4.2.300.jar


BIN
app/libs/agcp-1.6.0.300.jar


+ 4 - 3
app/src/androidTest/java/ch/threema/logging/backend/DebugLogFileBackendTest.java

@@ -36,6 +36,7 @@ import java.util.concurrent.TimeUnit;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.rule.GrantPermissionRule;
+import ch.threema.app.BuildConfig;
 import ch.threema.app.DangerousTest;
 
 /**
@@ -63,7 +64,7 @@ public class DebugLogFileBackendTest {
 
 		// Log with the debug log file disabled
 		final DebugLogFileBackend backend = new DebugLogFileBackend(Log.INFO);
-		backend.print(Log.WARN, "3ma", null, "hi");
+		backend.print(Log.WARN, BuildConfig.LOG_TAG, null, "hi");
 
 		// Enabling the debug log file won't create the log file just yet
 		Assert.assertFalse(logFilePath.exists());
@@ -71,11 +72,11 @@ public class DebugLogFileBackendTest {
 		Assert.assertFalse(logFilePath.exists());
 
 		// Logs below the min log level are filtered
-		backend.printAsync(Log.DEBUG, "3ma", null, "hey").get(500, TimeUnit.MILLISECONDS);
+		backend.printAsync(Log.DEBUG, BuildConfig.LOG_TAG, null, "hey").get(500, TimeUnit.MILLISECONDS);
 		Assert.assertFalse(logFilePath.exists());
 
 		// Log with the debug log file enabled
-		backend.printAsync(Log.WARN, "3ma", null, "hi").get(500, TimeUnit.MILLISECONDS);
+		backend.printAsync(Log.WARN, BuildConfig.LOG_TAG, null, "hi").get(500, TimeUnit.MILLISECONDS);
 		Assert.assertTrue(logFilePath.exists());
 	}
 

+ 5 - 3
app/src/foss_based/java/ch/threema/app/activities/VoiceActionActivity.java

@@ -21,9 +21,11 @@
 
 package ch.threema.app.activities;
 
-public class VoiceActionActivity {
+import android.app.Activity;
 
-	private VoiceActionActivity() {
-		// stub, no voice assistant api in foss build
+public class VoiceActionActivity extends Activity {
+
+	public VoiceActionActivity() {
+		// stub, no voice assistant api in hms build
 	}
 }

+ 15 - 3
app/src/foss_based/java/ch/threema/app/services/VoiceActionService.java

@@ -21,8 +21,20 @@
 
 package ch.threema.app.services;
 
-public class VoiceActionService {
-	private VoiceActionService() {
-		// stub, no voice api in foss build
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+import androidx.annotation.Nullable;
+
+public class VoiceActionService extends Service {
+	public VoiceActionService() {
+		// stub, no voice assistant api in hms build
+	}
+
+	@Nullable
+	@Override
+	public IBinder onBind(Intent intent) {
+		return null;
 	}
 }

+ 4 - 2
app/src/hms_services_based/java/ch/threema/app/activities/VoiceActionActivity.java

@@ -21,9 +21,11 @@
 
 package ch.threema.app.activities;
 
-public class VoiceActionActivity {
+import android.app.Activity;
 
-	private VoiceActionActivity() {
+public class VoiceActionActivity extends Activity {
+
+	public VoiceActionActivity() {
 		// stub, no voice assistant api in hms build
 	}
 }

+ 1 - 0
app/src/hms_services_based/java/ch/threema/app/licensing/StoreLicenseCheck.java

@@ -31,6 +31,7 @@ import org.slf4j.Logger;
 
 import ch.threema.app.routines.CheckLicenseRoutine;
 import ch.threema.app.services.UserService;
+import ch.threema.base.utils.LoggingUtil;
 
 public class StoreLicenseCheck implements CheckLicenseRoutine.StoreLicenseChecker {
 	private static final Logger logger = LoggingUtil.getThreemaLogger("StoreLicenseCheck");

+ 1 - 0
app/src/hms_services_based/java/ch/threema/app/push/PushRegistrationWorker.java

@@ -33,6 +33,7 @@ import androidx.work.Data;
 import androidx.work.Worker;
 import androidx.work.WorkerParameters;
 import ch.threema.app.utils.PushUtil;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.domain.protocol.csp.ProtocolDefines;
 
 public class PushRegistrationWorker extends Worker {

+ 1 - 0
app/src/hms_services_based/java/ch/threema/app/push/PushService.java

@@ -42,6 +42,7 @@ import ch.threema.app.ThreemaApplication;
 import ch.threema.app.utils.PushUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.base.ThreemaException;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.domain.protocol.csp.ProtocolDefines;
 
 import static ch.threema.app.push.PushRegistrationWorker.APP_ID_CONFIG_FIELD;

+ 13 - 2
app/src/hms_services_based/java/ch/threema/app/services/VoiceActionService.java

@@ -21,9 +21,20 @@
 
 package ch.threema.app.services;
 
-public class VoiceActionService {
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
 
-	private VoiceActionService() {
+import androidx.annotation.Nullable;
+
+public class VoiceActionService extends Service {
+	public VoiceActionService() {
 		// stub, no voice assistant api in hms build
 	}
+
+	@Nullable
+	@Override
+	public IBinder onBind(Intent intent) {
+		return null;
+	}
 }

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

@@ -155,8 +155,6 @@
 		</intent>
 	</queries>
 
-	<uses-sdk tools:overrideLibrary="androidx.camera.core, androidx.camera.camera2,
-		androidx.camera.lifecycle, androidx.camera.view, me.zhanghai.android.fastscroll" />
 
 	<application
 		android:name=".ThreemaApplication"
@@ -878,7 +876,6 @@
 			android:exported="false"/>
 		<service
 			android:name=".voip.services.CallRejectService"
-			android:permission="android.permission.BIND_JOB_SERVICE"
 			android:exported="false"/>
 		<service
 			android:name=".webclient.services.SessionAndroidService"
@@ -950,9 +947,6 @@
 			android:exported="false">
 		</receiver>
 		<receiver android:name=".receivers.FetchMessagesBroadcastReceiver"/>
-		<receiver
-			android:name=".voip.receivers.CallRejectReceiver"
-			android:exported="false"/>
 		<receiver android:name=".voip.receivers.VoipMediaButtonReceiver">
 			<intent-filter>
 				<action android:name="android.intent.action.MEDIA_BUTTON"/>

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

@@ -1064,7 +1064,7 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 	}
 
 	private static void initMapLibre() {
-		if (ConfigUtils.hasNoMapboxSupport()) {
+		if (ConfigUtils.hasNoMapLibreSupport()) {
 			logger.debug("*** MapLibre disabled due to faulty firmware");
 		} else {
 			Mapbox.getInstance(getAppContext());

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

@@ -152,6 +152,7 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 	private static final String DIALOG_TAG_FILECOPY = "filecopy";
 
 	public static final String INTENT_DATA_MULTISELECT = "ms";
+	public static final String INTENT_DATA_MULTISELECT_FOR_COMPOSE = "msi"; // allow multi select for composing a new message (automatically creates a distribution list)
 
 	private static final int REQUEST_READ_EXTERNAL_STORAGE = 1;
 
@@ -160,7 +161,7 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 	private MenuItem searchMenuItem;
 	private ThreemaSearchView searchView;
 
-	private boolean hideUi, hideRecents, multiSelect;
+	private boolean hideUi, hideRecents, multiSelect, multiSelectIdentities;
 	private String captionText;
 	private final List<MediaItem> mediaItems = new ArrayList<>();
 	private final List<MessageReceiver> recipientMessageReceivers = new ArrayList<>();
@@ -414,6 +415,7 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 			try {
 				this.hideRecents = intent.getBooleanExtra(ThreemaApplication.INTENT_DATA_HIDE_RECENTS, false);
 				this.multiSelect = intent.getBooleanExtra(INTENT_DATA_MULTISELECT, true);
+				this.multiSelectIdentities = intent.getBooleanExtra(INTENT_DATA_MULTISELECT_FOR_COMPOSE, false);
 			} catch (BadParcelableException e) {
 				logger.error("Exception", e);
 			}
@@ -1124,9 +1126,12 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 
 			Fragment fragment = null;
 
+			boolean allowMultiSelectForCompose = false;
+
 			switch (tabs.get(position)) {
 				case FRAGMENT_USERS:
 					fragment = new UserListFragment();
+					allowMultiSelectForCompose = multiSelectIdentities;
 					break;
 				case FRAGMENT_GROUPS:
 					fragment = new GroupListFragment();
@@ -1139,12 +1144,14 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 					break;
 				case FRAGMENT_WORK_USERS:
 					fragment = new WorkUserListFragment();
+					allowMultiSelectForCompose = multiSelectIdentities;
 					break;
 			}
 
 			if (fragment != null) {
 				Bundle args = new Bundle();
 				args.putBoolean(RecipientListFragment.ARGUMENT_MULTI_SELECT, multiSelect);
+				args.putBoolean(RecipientListFragment.ARGUMENT_MULTI_SELECT_FOR_COMPOSE, allowMultiSelectForCompose);
 				fragment.setArguments(args);
 			}
 

+ 7 - 3
app/src/main/java/ch/threema/app/adapters/ContactListAdapter.java

@@ -543,9 +543,13 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
 		ContactModel contactModel;
 
 		for (int position: checkedItems) {
-			contactModel = ovalues.get(position);
-			if (contactModel != null) {
-				contacts.add(contactModel);
+			try {
+				contactModel = ovalues.get(position);
+				if (contactModel != null) {
+					contacts.add(contactModel);
+				}
+			} catch (IndexOutOfBoundsException e) {
+				checkedItems.remove(position);
 			}
 		}
 		return contacts;

+ 2 - 1
app/src/main/java/ch/threema/app/adapters/UserListAdapter.java

@@ -49,7 +49,6 @@ import ch.threema.app.ui.CheckableView;
 import ch.threema.app.ui.VerificationLevelImageView;
 import ch.threema.app.ui.listitemholder.AvatarListItemHolder;
 import ch.threema.app.utils.AdapterUtil;
-import ch.threema.app.utils.ContactUtil;
 import ch.threema.app.utils.MessageUtil;
 import ch.threema.app.utils.NameUtil;
 import ch.threema.app.utils.TestUtil;
@@ -185,6 +184,8 @@ public class UserListAdapter extends FilterableListAdapter {
 				holder
 		);
 
+		position += ((ListView)parent).getHeaderViewsCount();
+
 		((ListView)parent).setItemChecked(position, checkedItems.contains(holder.originalPosition));
 
 		return itemView;

+ 1 - 1
app/src/main/java/ch/threema/app/adapters/decorators/LocationChatAdapterDecorator.java

@@ -149,7 +149,7 @@ public class LocationChatAdapterDecorator extends ChatAdapterDecorator {
 	}
 
 	private void viewLocation(AbstractMessageModel messageModel, final View v) {
-		if (!ConfigUtils.hasNoMapboxSupport()) {
+		if (!ConfigUtils.hasNoMapLibreSupport()) {
 			if (messageModel != null) {
 				LocationDataModel locationData = messageModel.getLocationData();
 				if (locationData != null) {

+ 3 - 3
app/src/main/java/ch/threema/app/backuprestore/csv/RestoreService.java

@@ -246,7 +246,7 @@ public class RestoreService extends Service {
 		} else {
 			logger.debug("onStartCommand intent == null");
 
-			onFinished(null);
+			onFinished("Empty intent");
 		}
 		isRunning = false;
 
@@ -754,7 +754,7 @@ public class RestoreService extends Service {
 								try (ZipInputStream inputStream = zipFile.getInputStream(thumbnailFileHeader)) {
 									byte[] thumbnailBytes = IOUtils.toByteArray(inputStream);
 									if (thumbnailBytes != null) {
-										this.fileService.writeConversationMediaThumbnail(model, thumbnailBytes);
+										this.fileService.saveThumbnail(model, thumbnailBytes);
 									}
 								}
 								//
@@ -775,7 +775,7 @@ public class RestoreService extends Service {
 
 								//if no thumbnail file exist in backup, generate one
 								if (thumbnailFileHeader == null && imageData != null) {
-									this.fileService.writeConversationMediaThumbnail(model, imageData);
+									this.fileService.saveThumbnail(model, imageData);
 								}
 							}
 						}

+ 52 - 22
app/src/main/java/ch/threema/app/camera/CameraFragment.kt

@@ -52,6 +52,7 @@ import androidx.core.content.ContextCompat
 import androidx.core.view.ViewCompat
 import androidx.core.view.WindowInsetsCompat
 import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
 import androidx.lifecycle.Lifecycle
 import androidx.localbroadcastmanager.content.LocalBroadcastManager
 import ch.threema.app.R
@@ -80,8 +81,9 @@ class CameraFragment : Fragment() {
     private val RECORDING_MODE_IMAGE = 0
     private val RECORDING_MODE_VIDEO = 1
 
+    private val viewModel: CameraFragmentViewModel by viewModels()
+
     private var displayId: Int = -1
-    private var lensFacing: Int = CameraSelector.LENS_FACING_BACK
     private var preview: Preview? = null
     private var imageCapture: ImageCapture? = null
     private var videoCapture: VideoCapture? = null
@@ -216,7 +218,6 @@ class CameraFragment : Fragment() {
         // Initialize our background executor
         cameraExecutor = Executors.newSingleThreadExecutor()
 
-        broadcastManager = LocalBroadcastManager.getInstance(view.context)
         broadcastManager = LocalBroadcastManager.getInstance(view.context)
 
         // Set up the intent filter that will receive events from our main activity
@@ -234,6 +235,11 @@ class CameraFragment : Fragment() {
         }
 
         previewView = container!!.findViewById(R.id.camera_view) as PreviewView
+        if (previewView == null) {
+            activity?.finish()
+            return
+        }
+
         previewView!!.setOnTouchListener { v, event ->
             v.performClick()
             scaleGestureDetector.onTouchEvent(event)
@@ -255,15 +261,16 @@ class CameraFragment : Fragment() {
 
         // Wait for the views to be properly laid out
         previewView!!.post {
+            if (previewView != null) {
+                // Keep track of the display in which this view is attached
+                displayId = previewView!!.display.displayId
 
-            // Keep track of the display in which this view is attached
-            displayId = previewView!!.display.displayId
-
-            // Build UI controls
-            updateCameraUi()
+                // Build UI controls
+                updateCameraUi()
 
-            // Set up the camera and its use cases
-            setUpCamera()
+                // Set up the camera and its use cases
+                setUpCamera()
+            }
         }
     }
 
@@ -303,10 +310,15 @@ class CameraFragment : Fragment() {
         super.onConfigurationChanged(newConfig)
 
         // Rebind the camera with the updated display metrics
-        bindCameraUseCases()
+        try {
+            bindCameraUseCases()
 
-        // Enable or disable switching between cameras
-        updateCameraSwitchButton()
+            // Enable or disable switching between cameras
+            updateCameraSwitchButton()
+        } catch (exc: Exception) {
+            logger.error("Use case binding failed", exc)
+            activity?.finish()
+        }
     }
 
     /** Initialize CameraX, and prepare to bind the camera use cases  */
@@ -318,10 +330,19 @@ class CameraFragment : Fragment() {
             cameraProvider = cameraProviderFuture.get()
 
             // Select lensFacing depending on the available cameras
-            lensFacing = when {
-                hasBackCamera() -> CameraSelector.LENS_FACING_BACK
-                hasFrontCamera() -> CameraSelector.LENS_FACING_FRONT
-                else -> throw IllegalStateException("Back and front camera are unavailable")
+            if (viewModel.lensFacing == CameraSelector.LENS_FACING_BACK && !hasBackCamera()) {
+                // try front camera
+                viewModel.lensFacing = CameraSelector.LENS_FACING_FRONT;
+            }
+
+            if (viewModel.lensFacing == CameraSelector.LENS_FACING_FRONT && !hasFrontCamera()) {
+                if (hasBackCamera()) {
+                    viewModel.lensFacing = CameraSelector.LENS_FACING_BACK;
+                } else {
+                    Toast.makeText(context, R.string.no_camera_installed, Toast.LENGTH_SHORT).show()
+                    logger.info("Back and front camera are unavailable")
+                    requireActivity().finish()
+                }
             }
 
             // Build and bind the camera use cases
@@ -342,6 +363,10 @@ class CameraFragment : Fragment() {
     /** Declare and bind preview, capture and analysis use cases */
     @SuppressLint("RestrictedApi")
     private fun bindCameraUseCases(): Boolean {
+        if (previewView == null) {
+            return false
+        }
+
         // Set the preferred aspect ratio as 4:3 if it is IMAGE only mode. Set the preferred aspect
         // ratio as 16:9 if it is VIDEO or MIXED mode. Then, it will be WYSIWYG when the view finder
         // is in CENTER_INSIDE mode.
@@ -351,7 +376,7 @@ class CameraFragment : Fragment() {
 
         val cameraProvider = cameraProvider
                 ?: throw IllegalStateException("Camera initialization failed.")
-        val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build()
+        val cameraSelector = CameraSelector.Builder().requireLensFacing(viewModel.lensFacing).build()
 
         val previewHeight = (previewView!!.measuredWidth / targetAspectRatio.toFloat()).toInt()
 
@@ -363,7 +388,6 @@ class CameraFragment : Fragment() {
                 .build()
         logger.debug("Preview size: {} * {} isDisplayPortrait {}", previewView!!.measuredWidth, previewHeight, isDisplayPortrait)
 
-
         val width: Int
         val height: Int
 
@@ -633,8 +657,15 @@ class CameraFragment : Fragment() {
                         return
                     }
 
+                    val metadata = ImageCapture.Metadata().apply {
+                        // Mirror image when using the front camera
+                        isReversedHorizontal = viewModel.lensFacing == CameraSelector.LENS_FACING_FRONT
+                    }
+
                     // Create output options object which contains file + metadata
-                    val outputOptions = ImageCapture.OutputFileOptions.Builder(File(cameraCallback?.cameraFilePath!!)).build()
+                    val outputOptions = ImageCapture.OutputFileOptions.Builder(File(cameraCallback?.cameraFilePath!!))
+                            .setMetadata(metadata)
+                            .build()
 
                     // Setup image capture listener which is triggered after photo has been taken
                     imageCapture.takePicture(outputOptions, cameraExecutor, object : ImageCapture.OnImageSavedCallback {
@@ -692,7 +723,7 @@ class CameraFragment : Fragment() {
 
             // Listener for button used to switch cameras. Only called if the button is enabled
             it.setOnClickListener {
-                lensFacing = if (lensFacing == CameraSelector.LENS_FACING_FRONT) CameraSelector.LENS_FACING_BACK else CameraSelector.LENS_FACING_FRONT
+                viewModel.lensFacing = if (viewModel.lensFacing == CameraSelector.LENS_FACING_FRONT) CameraSelector.LENS_FACING_BACK else CameraSelector.LENS_FACING_FRONT
 
                 // Re-bind use cases to update selected camera
                 bindCameraUseCases()
@@ -760,8 +791,7 @@ class CameraFragment : Fragment() {
         updateFlashButton()
     }
 
-
-    @SuppressLint("UnsafeExperimentalUsageError", "UnsafeOptInUsageError", "RestrictedApi")
+    @SuppressLint("UnsafeExperimentalUsageError", "UnsafeOptInUsageError", "RestrictedApi", "MissingPermission")
     private fun startVideoRecording() {
         if (cameraCallback?.videoFilePath == null) {
             return

+ 6 - 18
app/src/main/java/ch/threema/app/voip/receivers/CallRejectReceiver.java → app/src/main/java/ch/threema/app/camera/CameraFragmentViewModel.kt

@@ -4,7 +4,7 @@
  *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
  *
  * Threema for Android
- * Copyright (c) 2019-2022 Threema GmbH
+ * Copyright (c) 2022 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,
@@ -19,23 +19,11 @@
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
  */
 
-package ch.threema.app.voip.receivers;
+package ch.threema.app.camera
 
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
+import androidx.camera.core.CameraSelector
+import androidx.lifecycle.ViewModel
 
-import org.slf4j.Logger;
-
-import ch.threema.app.voip.services.CallRejectService;
-import ch.threema.base.utils.LoggingUtil;
-
-public class CallRejectReceiver extends BroadcastReceiver {
-	private static final Logger logger = LoggingUtil.getThreemaLogger("CallRejectReceiver");
-
-	@Override
-	public void onReceive(Context context, Intent intent) {
-		logger.info("onReceive");
-		CallRejectService.enqueueWork(context, intent);
-	}
+class CameraFragmentViewModel : ViewModel() {
+    var lensFacing: Int = CameraSelector.LENS_FACING_BACK
 }

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

@@ -36,4 +36,4 @@ public class EmojiCategory {
 		this.category = category;
 		this.emojiInfos = emojiInfos;
 	}
-}
+}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 3096 - 2490
app/src/main/java/ch/threema/app/emojis/EmojiParser.java


Dosya farkı çok büyük olduğundan ihmal edildi
+ 215 - 125
app/src/main/java/ch/threema/app/emojis/EmojiSpritemap.java


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

@@ -38,4 +38,4 @@ public class SpriteCoordinates {
 		this.x = x;
 		this.y = y;
 	}
-}
+}

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

@@ -834,18 +834,21 @@ public class ComposeMessageFragment extends Fragment implements
 
 			@Override
 			protected void onPostExecute(Boolean hasMoreRecords) {
-				if (messageModels != null) {
-					int numberOfInsertedRecords = insertToList(messageModels, false, true, true);
-					if (numberOfInsertedRecords > 0) {
-						convListView.setSelection(convListView.getSelectedItemPosition() + numberOfInsertedRecords + 1);
+				if (composeMessageAdapter != null) {
+					if (messageModels != null) {
+						int numberOfInsertedRecords = insertToList(messageModels, false, true, true);
+						if (numberOfInsertedRecords > 0) {
+							convListView.setSelection(convListView.getSelectedItemPosition() + numberOfInsertedRecords + 1);
+						}
+					} else {
+						composeMessageAdapter.notifyDataSetChanged();
 					}
-				} else {
-					composeMessageAdapter.notifyDataSetChanged();
 				}
 
-				// Notify PullToRefreshAttacher that the refresh has activity.finished
-				swipeRefreshLayout.setRefreshing(false);
-				swipeRefreshLayout.setEnabled(hasMoreRecords);
+				if (swipeRefreshLayout != null) {
+					swipeRefreshLayout.setRefreshing(false);
+					swipeRefreshLayout.setEnabled(hasMoreRecords);
+				}
 			}
 		}.execute();
 	}
@@ -3305,6 +3308,7 @@ public class ComposeMessageFragment extends Fragment implements
 
 		this.actionBarSubtitleTextView.setVisibility(View.GONE);
 		this.actionBarSubtitleImageView.setVisibility(View.GONE);
+		this.actionBarAvatarView.setVisibility(View.VISIBLE);
 
 		this.actionBarTitleTextView.setText(this.messageReceiver.getDisplayName());
 		this.actionBarTitleTextView.setPaintFlags(this.actionBarTitleTextView.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
@@ -3320,7 +3324,12 @@ public class ComposeMessageFragment extends Fragment implements
 		} else if (this.isDistributionListChat) {
 			actionBarSubtitleTextView.setText(this.distributionListService.getMembersString(this.distributionListModel));
 			actionBarSubtitleTextView.setVisibility(View.VISIBLE);
-			actionBarAvatarView.setImageBitmap(distributionListService.getAvatar(distributionListModel, false));
+			if (this.distributionListModel.isHidden()) {
+				actionBarAvatarView.setVisibility(View.GONE);
+				actionBarTitleTextView.setText(getString(R.string.threema_message_to, ""));
+			} else {
+				actionBarAvatarView.setImageBitmap(distributionListService.getAvatar(distributionListModel, false));
+			}
 			actionBarAvatarView.setBadgeVisible(false);
 		} else {
 			if (contactModel != null) {
@@ -3701,10 +3710,6 @@ public class ComposeMessageFragment extends Fragment implements
 								listener.onModifiedAll();
 							}
 						});
-
-						if (getActivity() != null) {
-							getActivity().finish();
-						}
 					}
 			}
 		}).execute();

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

@@ -80,6 +80,11 @@ public class DistributionListFragment extends RecipientListFragment {
 					public boolean sortingAscending() {
 						return false;
 					}
+
+					@Override
+					public boolean showHidden() {
+						return false;
+					}
 				});
 			}
 

+ 1 - 3
app/src/main/java/ch/threema/app/fragments/MessageSectionFragment.java

@@ -35,7 +35,6 @@ import android.graphics.PorterDuff;
 import android.graphics.Rect;
 import android.os.AsyncTask;
 import android.os.Bundle;
-import android.preference.PreferenceActivity;
 import android.text.Html;
 import android.text.format.DateUtils;
 import android.view.LayoutInflater;
@@ -55,7 +54,6 @@ import org.slf4j.Logger;
 import java.io.File;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 
 import androidx.annotation.NonNull;
@@ -103,7 +101,6 @@ import ch.threema.app.messagereceiver.ContactMessageReceiver;
 import ch.threema.app.messagereceiver.GroupMessageReceiver;
 import ch.threema.app.messagereceiver.MessageReceiver;
 import ch.threema.app.preference.SettingsActivity;
-import ch.threema.app.preference.SettingsSecurityFragment;
 import ch.threema.app.routines.SynchronizeContactsRoutine;
 import ch.threema.app.services.ContactService;
 import ch.threema.app.services.ConversationService;
@@ -1034,6 +1031,7 @@ public class MessageSectionFragment extends MainFragment
 		Intent intent = new Intent(getContext(), RecipientListBaseActivity.class);
 		intent.putExtra(ThreemaApplication.INTENT_DATA_HIDE_RECENTS, true);
 		intent.putExtra(RecipientListBaseActivity.INTENT_DATA_MULTISELECT, false);
+		intent.putExtra(RecipientListBaseActivity.INTENT_DATA_MULTISELECT_FOR_COMPOSE, true);
 		AnimationUtil.startActivityForResult(this.getActivity(), v, intent, ThreemaActivity.ACTIVITY_ID_COMPOSE_MESSAGE);
 	}
 

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

@@ -22,6 +22,8 @@
 package ch.threema.app.fragments;
 
 import android.content.Intent;
+import android.graphics.Rect;
+import android.graphics.Typeface;
 import android.os.Bundle;
 import android.os.Parcelable;
 import android.view.LayoutInflater;
@@ -33,14 +35,20 @@ import android.widget.ImageView;
 import android.widget.ListView;
 import android.widget.ProgressBar;
 import android.widget.TextView;
+import android.widget.Toast;
 
+import com.getkeepsafe.taptargetview.TapTarget;
+import com.getkeepsafe.taptargetview.TapTargetView;
 import com.google.android.material.appbar.AppBarLayout;
 import com.google.android.material.floatingactionbutton.FloatingActionButton;
 import com.google.android.material.snackbar.Snackbar;
 
+import org.slf4j.Logger;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.List;
 
 import androidx.annotation.DrawableRes;
 import androidx.annotation.NonNull;
@@ -68,12 +76,16 @@ import ch.threema.app.utils.LogUtil;
 import ch.threema.app.utils.NameUtil;
 import ch.threema.app.utils.SnackbarUtil;
 import ch.threema.base.ThreemaException;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.DistributionListModel;
 import ch.threema.storage.models.GroupModel;
 
 public abstract class RecipientListFragment extends ListFragment implements ListView.OnItemLongClickListener {
 	public static final String ARGUMENT_MULTI_SELECT = "ms";
+	public static final String ARGUMENT_MULTI_SELECT_FOR_COMPOSE = "msi";
+
+	private static final Logger logger = LoggingUtil.getThreemaLogger("RecipientListFragment");
 
 	protected ContactService contactService;
 	protected GroupService groupService;
@@ -88,7 +100,7 @@ public abstract class RecipientListFragment extends ListFragment implements List
 	protected Snackbar snackbar;
 	protected ProgressBar progressBar;
 	protected View topLayout;
-	protected boolean multiSelect = true;
+	protected boolean multiSelect = true, multiSelectIdentity = false;
 	protected FilterableListAdapter adapter;
 
 	private boolean isVisible = false;
@@ -116,6 +128,7 @@ public abstract class RecipientListFragment extends ListFragment implements List
 		Bundle bundle = getArguments();
 		if (bundle != null) {
 			multiSelect = bundle.getBoolean(ARGUMENT_MULTI_SELECT, true);
+			multiSelectIdentity = bundle.getBoolean(ARGUMENT_MULTI_SELECT_FOR_COMPOSE, false);
 		}
 
 		topLayout = inflater.inflate(R.layout.fragment_list, container, false);
@@ -173,6 +186,51 @@ public abstract class RecipientListFragment extends ListFragment implements List
 					onFloatingActionButtonClick();
 				}
 			});
+
+			if (multiSelectIdentity) {
+				floatingActionButton.setImageResource(R.drawable.ic_arrow_left);
+				floatingActionButton.setRotation(180);
+
+				if (preferenceService.getMultipleRecipientsTooltipCount() < 1) {
+					preferenceService.incrementMultipleRecipientsTooltipCount();
+
+					getListView().post(() -> {
+						try {
+							int[] location = new int[2];
+							getListView().getLocationOnScreen(location);
+
+							int itemHeight = getResources().getDimensionPixelSize(R.dimen.messagelist_item_height);
+
+							Rect rect = new Rect(
+								location[0] + 200,
+								location[1] + itemHeight,
+								location[0] + 200 + itemHeight,
+								location[1] + (itemHeight * 2));
+
+							TapTargetView.showFor(requireActivity(),
+								TapTarget.forBounds(rect, getString(R.string.tooltip_multiple_recipients_title), getString(R.string.tooltip_multiple_recipients_text))
+									.outerCircleColor(ConfigUtils.getAppTheme(getActivity()) == ConfigUtils.THEME_DARK ? R.color.dark_accent : R.color.accent_light)      // Specify a color for the outer circle
+									.outerCircleAlpha(0.96f)            // Specify the alpha amount for the outer circle
+									.targetCircleColor(android.R.color.transparent)   // Specify a color for the target circle
+									.titleTextSize(24)                  // Specify the size (in sp) of the title text
+									.titleTextColor(android.R.color.white)      // Specify the color of the title text
+									.descriptionTextSize(18)            // Specify the size (in sp) of the description text
+									.descriptionTextColor(android.R.color.white)  // Specify the color of the description text
+									.textColor(android.R.color.white)            // Specify a color for both the title and description text
+									.textTypeface(Typeface.SANS_SERIF)  // Specify a typeface for the text
+									.dimColor(android.R.color.black)            // If set, will dim behind the view with 30% opacity of the given color
+									.drawShadow(true)                   // Whether to draw a drop shadow or not
+									.cancelable(true)                  // Whether tapping outside the outer circle dismisses the view
+									.tintTarget(true)                   // Whether to tint the target view's color
+									.transparentTarget(true)           // Specify whether the target is transparent (displays the content underneath)
+									.targetRadius(50),                  // Specify the target radius (in dp)
+								null);
+						} catch (Exception ignore) {
+							// catch null typeface exception on CROSSCALL Action-X3
+						}
+					});
+				}
+			}
 		} else {
 			floatingActionButton.hide();
 		}
@@ -232,7 +290,11 @@ public abstract class RecipientListFragment extends ListFragment implements List
 		if (getListView().getChoiceMode() == AbsListView.CHOICE_MODE_MULTIPLE) {
 			if (getAdapter().getCheckedItemCount() > 0) {
 				if (snackbar != null) {
-					snackbar.setText(getString(R.string.really_send, getRecipientList()));
+					snackbar.setText(getString(
+						multiSelectIdentity ?
+						R.string.threema_message_to :
+						R.string.really_send,
+						getRecipientList()));
 				}
 			}
 		}
@@ -261,7 +323,37 @@ public abstract class RecipientListFragment extends ListFragment implements List
 	private void onFloatingActionButtonClick() {
 		final HashSet<?> objects = adapter.getCheckedItems();
 		if (!objects.isEmpty()) {
-			((RecipientListBaseActivity) activity).prepareForwardingOrSharing(new ArrayList<>(objects));
+			if (multiSelectIdentity) {
+				ContactModel contactModel = null;
+				List<String> identities = new ArrayList<>();
+				for (Object object: objects) {
+					if (object instanceof ContactModel) {
+						contactModel = (ContactModel) object;
+						identities.add(contactModel.getIdentity());
+					}
+				}
+
+				if (identities.size() > 1) {
+					try {
+						DistributionListModel distributionListModel = distributionListService.createDistributionList(
+							null,
+							identities.toArray(new String[0]),
+							true);
+
+						((RecipientListBaseActivity) activity).prepareForwardingOrSharing(new ArrayList<>(Collections.singleton(distributionListModel)));
+
+						return;
+					} catch (ThreemaException e) {
+						logger.error("Unable to create distribution list", e);
+					}
+				} else if (identities.size() == 1) {
+					((RecipientListBaseActivity) activity).prepareForwardingOrSharing(new ArrayList<>(Collections.singletonList(contactModel)));
+					return;
+				}
+				Toast.makeText(requireContext(), R.string.contact_not_found, Toast.LENGTH_LONG).show();
+			} else {
+				((RecipientListBaseActivity) activity).prepareForwardingOrSharing(new ArrayList<>(objects));
+			}
 		}
 	}
 

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

@@ -40,7 +40,7 @@ import ch.threema.storage.models.ContactModel;
 public class UserListFragment extends RecipientListFragment {
 	@Override
 	protected boolean isMultiSelectAllowed() {
-		return multiSelect;
+		return multiSelect || multiSelectIdentity;
 	}
 
 	@Override

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

@@ -50,7 +50,7 @@ import ch.threema.storage.models.ContactModel;
 public class WorkUserListFragment extends RecipientListFragment {
 	@Override
 	protected boolean isMultiSelectAllowed() {
-		return multiSelect;
+		return multiSelect || multiSelectIdentity;
 	}
 
 	@Override

+ 7 - 1
app/src/main/java/ch/threema/app/globalsearch/GlobalSearchActivity.java

@@ -56,6 +56,7 @@ import ch.threema.app.services.GroupService;
 import ch.threema.app.ui.ThreemaSearchView;
 import ch.threema.app.utils.AnimationUtil;
 import ch.threema.app.utils.ConfigUtils;
+import ch.threema.app.utils.EditTextUtil;
 import ch.threema.base.utils.LoggingUtil;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.DistributionListMessageModel;
@@ -78,6 +79,7 @@ public class GlobalSearchActivity extends ThreemaToolbarActivity implements Thre
 	private GlobalSearchAdapter chatsAdapter;
 	private GlobalSearchViewModel chatsViewModel;
 	private TextView emptyTextView;
+	private ThreemaSearchView searchView;
 	private ProgressBar progressBar;
 	private DeadlineListService hiddenChatsListService;
 	private ContactService contactService;
@@ -182,7 +184,7 @@ public class GlobalSearchActivity extends ThreemaToolbarActivity implements Thre
 
 		getWindow().setStatusBarColor(ConfigUtils.getColorFromAttribute(GlobalSearchActivity.this, R.attr.attach_status_bar_color_collapsed));
 
-		ThreemaSearchView searchView = findViewById(R.id.search);
+		searchView = findViewById(R.id.search);
 		searchView.setOnQueryTextListener(this);
 
 		emptyTextView = findViewById(R.id.empty_text);
@@ -257,6 +259,10 @@ public class GlobalSearchActivity extends ThreemaToolbarActivity implements Thre
 			return;
 		}
 
+		if (searchView != null) {
+			EditTextUtil.hideSoftKeyboard(searchView);
+		}
+
 		Intent intent = new Intent(this, ComposeMessageActivity.class);
 
 		if (messageModel instanceof GroupMessageModel) {

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

@@ -363,7 +363,7 @@ public class MediaAttachActivity extends MediaSelectionBaseActivity implements V
 		switch (id) {
 			case R.id.attach_location:
 				if (ConfigUtils.requestLocationPermissions(this, null, PERMISSION_REQUEST_LOCATION)) {
-					if (!ConfigUtils.hasNoMapboxSupport()) {
+					if (!ConfigUtils.hasNoMapLibreSupport()) {
 						launchPlacePicker();
 					} else {
 						Toast.makeText(this, "Feature not available due to firmware error", Toast.LENGTH_LONG).show();

+ 1 - 1
app/src/main/java/ch/threema/app/messagereceiver/DistributionListContactMessageReceiver.java

@@ -47,7 +47,7 @@ public class DistributionListContactMessageReceiver extends ContactMessageReceiv
 		SymmetricEncryptionResult encryptionResultIgnored,
 		MessageModel messageModel
 	) throws ThreemaException {
-		if (thumbnailBlobId == null || fileBlobId == null || fileEncryptionResult == null) {
+		if (fileBlobId == null || fileEncryptionResult == null) {
 			throw new ThreemaException("Required values have not been set by responsible DistributionListMessageReceiver");
 		}
 		return super.createBoxedFileMessage(thumbnailBlobId, fileBlobId, fileEncryptionResult, messageModel);

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

@@ -71,7 +71,7 @@ public class NotificationBuilderWrapper extends NotificationCompat.Builder {
 	public static long[] VIBRATE_PATTERN_INCOMING_CALL = new long[]{0, 1000, 1000, 0};
 	public static long[] VIBRATE_PATTERN_SILENT = new long[]{0};
 
-	private NotificationService.NotificationSchema notificationSchema;
+	private final NotificationService.NotificationSchema notificationSchema;
 
 	private static String newChannelId;
 

+ 7 - 3
app/src/main/java/ch/threema/app/preference/SettingsAboutFragment.kt

@@ -41,6 +41,7 @@ import ch.threema.base.utils.LoggingUtil
 
 private val logger = LoggingUtil.getThreemaLogger("SettingsAboutFragment")
 
+@Suppress("unused")
 class SettingsAboutFragment : ThreemaPreferenceFragment() {
 
     private var updateUrl: String? = null
@@ -48,8 +49,8 @@ class SettingsAboutFragment : ThreemaPreferenceFragment() {
 
     private var aboutCounter = 0
 
-    private var preferenceService: PreferenceService = getInitialPreferenceService()
-    private var licenseService: LicenseService<*> = getInitialLicenceService()
+    private var preferenceService: PreferenceService = requirePreferenceService()
+    private var licenseService: LicenseService<*> = requireLicenceService()
 
     override fun initializePreferences() {
         super.initializePreferences()
@@ -189,7 +190,7 @@ class SettingsAboutFragment : ThreemaPreferenceFragment() {
 
     private fun getVersionString(): String {
         val version = StringBuilder()
-        version.append(ConfigUtils.getFullAppVersion(requireContext()))
+        version.append(ConfigUtils.getAppVersion(requireContext()))
         version.append(" Build ").append(ConfigUtils.getBuildNumber(context))
         version.append(" ").append(BuildFlavor.getName())
         if (BuildConfig.DEBUG) {
@@ -205,10 +206,12 @@ class SettingsAboutFragment : ThreemaPreferenceFragment() {
             return
         }
         object : AsyncTask<Void?, Void?, String?>() {
+            @Deprecated("Deprecated in Java")
             override fun onPreExecute() {
                 GenericProgressDialog.newInstance(R.string.check_updates, R.string.please_wait).show(activity!!.supportFragmentManager, DIALOG_TAG_CHECK_UPDATE)
             }
 
+            @Deprecated("Deprecated in Java")
             override fun doInBackground(vararg voids: Void?): String? {
                 return try {
                     // Validate license and check for updates
@@ -229,6 +232,7 @@ class SettingsAboutFragment : ThreemaPreferenceFragment() {
                 }
             }
 
+            @Deprecated("Deprecated in Java")
             override fun onPostExecute(error: String?) {
                 DialogUtil.dismissDialog(parentFragmentManager, DIALOG_TAG_CHECK_UPDATE, true)
                 if (error != null) {

+ 4 - 7
app/src/main/java/ch/threema/app/preference/SettingsActivity.kt

@@ -27,9 +27,8 @@ import android.view.MenuItem
 import android.view.View
 import androidx.annotation.StringRes
 import androidx.fragment.app.Fragment
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleObserver
-import androidx.lifecycle.OnLifecycleEvent
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
 import androidx.preference.Preference
 import androidx.preference.PreferenceFragmentCompat
 import ch.threema.app.R
@@ -134,12 +133,10 @@ class SettingsActivity : ThreemaToolbarActivity(), PreferenceFragmentCompat.OnPr
         transaction.commit()
 
         if (!ConfigUtils.isTabletLayout()) {
-            fragment.lifecycle.addObserver(object : LifecycleObserver {
+            fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
                 // When the fragment is resumed, the correct title is set. This is needed for nested
                 // preference fragments such as the about fragment (when coming back from troubleshooting)
-                @Suppress("unused")
-                @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
-                fun fragmentIsResumed() {
+                override fun onResume(owner: LifecycleOwner) {
                     supportActionBar?.title = pref.title
                 }
             })

+ 4 - 3
app/src/main/java/ch/threema/app/preference/SettingsAppearanceFragment.kt

@@ -36,11 +36,12 @@ import ch.threema.app.utils.AppRestrictionUtil
 import ch.threema.app.utils.ConfigUtils
 import ch.threema.app.utils.StateBitmapUtil
 
+@Suppress("unused")
 class SettingsAppearanceFragment : ThreemaPreferenceFragment() {
 
     private var oldTheme: Int = 0
 
-    private val wallpaperService: WallpaperService = getInitialWallpaperService()
+    private val wallpaperService: WallpaperService = requireWallpaperService()
 
     private var showBadge: CheckBoxPreference? = null
     private var showBadgeChecked = false
@@ -166,8 +167,8 @@ class SettingsAppearanceFragment : ThreemaPreferenceFragment() {
                         dialog.setData(newEmojiStyle)
                         dialog.setCallback(object : GenericAlertDialog.DialogClickListener {
                             override fun onYes(tag: String?, data: Any?) {
-                                ConfigUtils.setEmojiStyle(activity, android.R.attr.data)
-                                updateEmojiPrefs(android.R.attr.data)
+                                ConfigUtils.setEmojiStyle(activity, data as Int)
+                                updateEmojiPrefs(data)
                                 ConfigUtils.recreateActivity(activity)
                             }
                             override fun onNo(tag: String?, data: Any?) {

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

@@ -32,6 +32,7 @@ import ch.threema.app.R
 import ch.threema.app.utils.AppRestrictionUtil
 import ch.threema.app.utils.ConfigUtils
 
+@Suppress("unused")
 class SettingsCallsFragment : ThreemaPreferenceFragment() {
 
     private var fragmentView: View? = null
@@ -100,6 +101,7 @@ class SettingsCallsFragment : ThreemaPreferenceFragment() {
         super.onViewCreated(view, savedInstanceState)
     }
 
+    @Deprecated("Deprecated in Java")
     override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) {
         when (requestCode) {
             PERMISSION_REQUEST_READ_PHONE_STATE -> if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

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

@@ -23,6 +23,7 @@ package ch.threema.app.preference
 
 import ch.threema.app.R
 
+@Suppress("unused")
 class SettingsChatFragment : ThreemaPreferenceFragment() {
     override fun getPreferenceResource() = R.xml.preference_chat
 }

+ 3 - 2
app/src/main/java/ch/threema/app/preference/SettingsDeveloperFragment.java

@@ -54,6 +54,7 @@ import ch.threema.storage.DatabaseServiceNew;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.data.status.VoipStatusDataModel;
 
+@SuppressWarnings("unused")
 public class SettingsDeveloperFragment extends ThreemaPreferenceFragment {
 	private static final Logger logger = LoggingUtil.getThreemaLogger("SettingsDeveloperFragment");
 
@@ -123,8 +124,8 @@ public class SettingsDeveloperFragment extends ThreemaPreferenceFragment {
 
 		// Pojo for holding test data.
 		class VoipMessage {
-			VoipStatusDataModel dataModel;
-			String description;
+			final VoipStatusDataModel dataModel;
+			final String description;
 			VoipMessage(VoipStatusDataModel dataModel, String description) {
 				this.dataModel = dataModel;
 				this.description = description;

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

@@ -39,7 +39,7 @@ import ch.threema.app.utils.ConfigUtils.THEME_DARK
 class SettingsFragment(private val onResumeCallback: () -> Unit = {}) : ThreemaPreferenceFragment() {
     private var preferencePairs: List<Pair<Preference, String>> = listOf()
     private var selectedPrefView: View? = null
-    private val preferenceService = getInitialPreferenceService()
+    private val preferenceService = requirePreferenceService()
 
     override fun onResume() {
         super.onResume()

+ 1 - 1
app/src/main/java/ch/threema/app/preference/SettingsMediaDummyActivity.java

@@ -23,7 +23,7 @@ package ch.threema.app.preference;
 
 import android.content.Intent;
 import android.os.Bundle;
-import android.preference.PreferenceActivity;
+
 import androidx.appcompat.app.AppCompatActivity;
 
 // Frontend to call the app's media settings directly from notification or system settings

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

@@ -37,7 +37,7 @@ class SettingsMediaFragment : ThreemaPreferenceFragment() {
 
     private var saveMediaPreference: Preference? = null
 
-    private val preferenceService = getInitialPreferenceService()
+    private val preferenceService = requirePreferenceService()
 
     override fun initializePreferences() {
         super.initializePreferences()

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

@@ -23,7 +23,6 @@ package ch.threema.app.preference;
 
 import android.content.Intent;
 import android.os.Bundle;
-import android.preference.PreferenceActivity;
 
 import androidx.appcompat.app.AppCompatActivity;
 import ch.threema.app.ThreemaApplication;

+ 8 - 7
app/src/main/java/ch/threema/app/preference/SettingsNotificationsFragment.java

@@ -130,6 +130,7 @@ public class SettingsNotificationsFragment extends ThreemaPreferenceFragment imp
 		MultiSelectListPreference multiSelectListPreference = getPref(getString(R.string.preferences__working_days));
 		multiSelectListPreference.setEntries(weekdays);
 		multiSelectListPreference.setOnPreferenceChangeListener((preference, newValue) -> {
+			//noinspection unchecked
 			updateWorkingDaysSummary(preference, (Set<String>) newValue);
 			return true;
 		});
@@ -243,7 +244,7 @@ public class SettingsNotificationsFragment extends ThreemaPreferenceFragment imp
 		int miuiVersion = ConfigUtils.getMIUIVersion();
 		if (miuiVersion < 10) {
 			PreferenceScreen preferenceScreen = getPref("pref_key_notifications");
-			preferenceScreen.removePreference(findPreference("pref_key_miui"));
+			preferenceScreen.removePreference(getPref("pref_key_miui"));
 		}
 
 		notificationManagerCompat = NotificationManagerCompat.from(requireActivity());
@@ -276,7 +277,7 @@ public class SettingsNotificationsFragment extends ThreemaPreferenceFragment imp
 		});
 		voiceRingtonePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
 			@Override
-			public boolean onPreferenceClick(Preference preference) {
+			public boolean onPreferenceClick(@NonNull Preference preference) {
 				chooseRingtone(RingtoneManager.TYPE_RINGTONE,
 					getRingtoneFromRingtonePref(R.string.preferences__voip_ringtone),
 					RingtoneUtil.THREEMA_CALL_RINGTONE_URI,
@@ -289,7 +290,7 @@ public class SettingsNotificationsFragment extends ThreemaPreferenceFragment imp
 		TwoStatePreference systemRingtonePreference = getPref(getString(R.string.preferences__use_system_ringtone));
 		systemRingtonePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
 			@Override
-			public boolean onPreferenceChange(Preference preference, Object newValue) {
+			public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
 				boolean newCheckedValue = newValue.equals(true);
 
 				if (newCheckedValue) {
@@ -316,7 +317,7 @@ public class SettingsNotificationsFragment extends ThreemaPreferenceFragment imp
 				R.string.miui_notification_title,
 				miuiVersion >= 12 ?
 					R.string.miui12_notification_body :
-					R.string.miui_notification_body).show(getFragmentManager(), DIALOG_TAG_MIUI_NOTICE);
+					R.string.miui_notification_body).show(getParentFragmentManager(), DIALOG_TAG_MIUI_NOTICE);
 
 			Preference miuiPreference = getPref("pref_key_miui");
 			miuiPreference.setOnPreferenceClickListener(preference -> {
@@ -350,7 +351,7 @@ public class SettingsNotificationsFragment extends ThreemaPreferenceFragment imp
 				true,
 				true);
 			dialog.setTargetFragment(SettingsNotificationsFragment.this, 0);
-			dialog.show(getFragmentManager(), tag);
+			dialog.show(getParentFragmentManager(), tag);
 		}
 
 	}
@@ -367,7 +368,7 @@ public class SettingsNotificationsFragment extends ThreemaPreferenceFragment imp
 	private void showNotificationsDisabledDialog() {
 		GenericAlertDialog dialog = GenericAlertDialog.newInstance(R.string.notifications_disabled_title, R.string.notifications_disabled_text, R.string.notifications_disabled_settings, R.string.cancel);
 		dialog.setTargetFragment(this, 0);
-		dialog.show(getFragmentManager(), DIALOG_TAG_NOTIFICATIONS_DISABLED);
+		dialog.show(getParentFragmentManager(), DIALOG_TAG_NOTIFICATIONS_DISABLED);
 	}
 
 	private Uri getRingtoneFromRingtonePref(@StringRes int preference) {
@@ -380,7 +381,7 @@ public class SettingsNotificationsFragment extends ThreemaPreferenceFragment imp
 	}
 
 	private void updateRingtoneSummary(Preference preference, String value) {
-		String summary = null;
+		String summary;
 		if (value == null || value.length() == 0) {
 			summary = getString(R.string.ringtone_none);
 		} else {

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

@@ -29,6 +29,7 @@ import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
+import android.widget.Toast
 import androidx.preference.CheckBoxPreference
 import androidx.preference.Preference
 import androidx.preference.PreferenceCategory
@@ -52,7 +53,7 @@ import com.google.android.material.snackbar.BaseTransientBottomBar.BaseCallback
 import com.google.android.material.snackbar.Snackbar
 
 
-class SettingsPrivacyFragment : ThreemaPreferenceFragment() {
+class SettingsPrivacyFragment : ThreemaPreferenceFragment(), GenericAlertDialog.DialogClickListener {
 
     private lateinit var disableScreenshot: CheckBoxPreference
     private var disableScreenshotChecked = false
@@ -61,7 +62,7 @@ class SettingsPrivacyFragment : ThreemaPreferenceFragment() {
 
     private lateinit var contactSyncPreference: TwoStatePreference
 
-    private val synchronizeContactsService: SynchronizeContactsService? = getOrNull { getInitialSynchronizeContactsService() }
+    private val synchronizeContactsService: SynchronizeContactsService? = getOrNull { requireSynchronizeContactsService() }
 
     private val synchronizeContactsListener: SynchronizeContactsListener = object : SynchronizeContactsListener {
         override fun onStarted(startedRoutine: SynchronizeContactsRoutine) {
@@ -94,7 +95,7 @@ class SettingsPrivacyFragment : ThreemaPreferenceFragment() {
         super.initializePreferences()
 
         disableScreenshot = getPref(R.string.preferences__hide_screenshots)
-        disableScreenshotChecked = this.disableScreenshot.isChecked ?: false
+        disableScreenshotChecked = this.disableScreenshot.isChecked
 
         if (ConfigUtils.getScreenshotsDisabled(ThreemaApplication.getServiceManager()?.preferenceService,
                         ThreemaApplication.getServiceManager()?.lockAppService)) {
@@ -246,6 +247,7 @@ class SettingsPrivacyFragment : ThreemaPreferenceFragment() {
         }
     }
 
+    @Deprecated("Deprecated in Java")
     override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) {
         when (requestCode) {
             PERMISSION_REQUEST_CONTACTS -> if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
@@ -265,7 +267,7 @@ class SettingsPrivacyFragment : ThreemaPreferenceFragment() {
     }
 
     private fun enableSync() {
-        getOrNull { getInitialSynchronizeContactsService() }?.apply {
+        getOrNull { requireSynchronizeContactsService() }?.apply {
             try {
                 if (enableSync() && ConfigUtils.requestContactPermissions(requireActivity(), this@SettingsPrivacyFragment, PERMISSION_REQUEST_CONTACTS)) {
                     launchContactsSync()
@@ -279,16 +281,25 @@ class SettingsPrivacyFragment : ThreemaPreferenceFragment() {
     }
 
     private fun disableSync() {
-        getOrNull { getInitialSynchronizeContactsService() }?.apply {
+        getOrNull { requireSynchronizeContactsService() }?.apply {
             GenericProgressDialog.newInstance(R.string.app_name, R.string.please_wait).show(parentFragmentManager, DIALOG_TAG_DISABLE_SYNC)
-            Thread {
+            Thread ({
                 disableSync {
                     RuntimeUtil.runOnUiThread {
                         DialogUtil.dismissDialog(parentFragmentManager, DIALOG_TAG_DISABLE_SYNC, true)
                         contactSyncPreference.isChecked = false
                     }
                 }
-            }.start()
+            }, "DisableSync").start()
+        }
+    }
+
+    private fun resetReceipts() {
+        getOrNull { requireContactService() }?.apply {
+            Thread ({
+                resetReceiptsSettings()
+                RuntimeUtil.runOnUiThread { Toast.makeText(context, R.string.reset_successful, Toast.LENGTH_SHORT).show() }
+            }, "ResetReceiptSettings").start()
         }
     }
 
@@ -300,4 +311,10 @@ class SettingsPrivacyFragment : ThreemaPreferenceFragment() {
 
         private const val PERMISSION_REQUEST_CONTACTS = 1
     }
+
+    override fun onYes(tag: String?, data: Any?) {
+        resetReceipts()
+    }
+
+    override fun onNo(tag: String?, data: Any?) { }
 }

+ 6 - 10
app/src/main/java/ch/threema/app/preference/SettingsRateFragment.java

@@ -43,7 +43,7 @@ public class SettingsRateFragment extends ThreemaPreferenceFragment implements R
 	protected void initializePreferences() {
 		RateDialog rateDialog = RateDialog.newInstance(getString(R.string.rate_title));
 		rateDialog.setTargetFragment(SettingsRateFragment.this, 0);
-		rateDialog.show(getFragmentManager(), DIALOG_TAG_RATE);
+		rateDialog.show(getParentFragmentManager(), DIALOG_TAG_RATE);
 	}
 
 	private boolean startRating(Uri uri) throws ActivityNotFoundException {
@@ -75,15 +75,11 @@ public class SettingsRateFragment extends ThreemaPreferenceFragment implements R
 
 	@Override
 	public void onYes(String tag, Object data) {
-		switch (tag) {
-			case DIALOG_TAG_RATE_ON_GOOGLE_PLAY:
-				if (!startRating(Uri.parse("market://details?id=" + BuildConfig.APPLICATION_ID))) {
-					startRating(Uri.parse("https://play.google.com/store/apps/details?id=" + BuildConfig.APPLICATION_ID));
-				}
-				requireActivity().onBackPressed();
-				break;
-			default:
-				break;
+		if (DIALOG_TAG_RATE_ON_GOOGLE_PLAY.equals(tag)) {
+			if (!startRating(Uri.parse("market://details?id=" + BuildConfig.APPLICATION_ID))) {
+				startRating(Uri.parse("https://play.google.com/store/apps/details?id=" + BuildConfig.APPLICATION_ID));
+			}
+			requireActivity().onBackPressed();
 		}
 	}
 

+ 21 - 32
app/src/main/java/ch/threema/app/preference/SettingsSecurityFragment.java

@@ -96,8 +96,8 @@ public class SettingsSecurityFragment extends ThreemaPreferenceFragment implemen
 
 		super.initializePreferences();
 
-		preferenceService = ThreemaApplication.getServiceManager().getPreferenceService();
-		hiddenChatsListService = ThreemaApplication.getServiceManager().getHiddenChatsListService();
+		preferenceService = requirePreferenceService();
+		hiddenChatsListService = requireHiddenChatListService();
 	}
 
 	private void onCreateUnlocked() {
@@ -155,7 +155,7 @@ public class SettingsSecurityFragment extends ThreemaPreferenceFragment implemen
 
 		lockMechanismPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
 			@Override
-			public boolean onPreferenceChange(Preference preference, Object newValue) {
+			public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
 				switch ((String) newValue) {
 					case LockingMech_NONE:
 						if (hiddenChatsListService.getSize() > 0) {
@@ -169,7 +169,7 @@ public class SettingsSecurityFragment extends ThreemaPreferenceFragment implemen
 					case PreferenceService.LockingMech_PIN:
 						GenericAlertDialog dialog = GenericAlertDialog.newInstance(R.string.warning, getString(R.string.password_remember_warning, getString(R.string.app_name)), R.string.ok, R.string.cancel);
 						dialog.setTargetFragment(SettingsSecurityFragment.this, 0);
-						dialog.show(getFragmentManager(), DIALOG_TAG_PASSWORD_REMINDER_PIN);
+						dialog.show(getParentFragmentManager(), DIALOG_TAG_PASSWORD_REMINDER_PIN);
 						break;
 					case PreferenceService.LockingMech_SYSTEM:
 						setSystemScreenLock();
@@ -184,7 +184,7 @@ public class SettingsSecurityFragment extends ThreemaPreferenceFragment implemen
 		});
 
 		uiLockSwitchPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
-			public boolean onPreferenceChange(Preference preference, Object newValue) {
+			public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
 				boolean newCheckedValue = newValue.equals(true);
 
 				if (((TwoStatePreference) preference).isChecked() != newCheckedValue) {
@@ -205,7 +205,7 @@ public class SettingsSecurityFragment extends ThreemaPreferenceFragment implemen
 
 		pinPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
 			@Override
-			public boolean onPreferenceClick(Preference preference) {
+			public boolean onPreferenceClick(@NonNull Preference preference) {
 				if (preference.getKey().equals(getResources().getString(R.string.preferences__pin_lock_code))) {
 					if (preferenceService.isPinSet()) {
 						setPin();
@@ -217,16 +217,16 @@ public class SettingsSecurityFragment extends ThreemaPreferenceFragment implemen
 
 		this.updateGracePreferenceSummary(gracePreference.getValue());
 		gracePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
-			public boolean onPreferenceChange(Preference preference, Object newValue) {
+			public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
 				updateGracePreferenceSummary(newValue.toString());
 				return true;
 			}
 		});
 
-		masterkeyPreference = findPreference(getResources().getString(R.string.preferences__masterkey_passphrase));
+		masterkeyPreference = getPref(getResources().getString(R.string.preferences__masterkey_passphrase));
 		masterkeyPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
 			@Override
-			public boolean onPreferenceClick(Preference preference) {
+			public boolean onPreferenceClick(@NonNull Preference preference) {
 				if (MessageDigest.isEqual(preference.getKey().getBytes(), getResources().getString(R.string.preferences__masterkey_passphrase).getBytes())) {
 					Intent intent = new Intent(getActivity(), UnlockMasterKeyActivity.class);
 					intent.putExtra(ThreemaApplication.INTENT_DATA_PASSPHRASE_CHECK, true);
@@ -240,13 +240,13 @@ public class SettingsSecurityFragment extends ThreemaPreferenceFragment implemen
 		masterkeySwitchPreference = findPreference(getResources().getString(R.string.preferences__masterkey_switch));
 
 		//fix wrong state
-		if (masterkeySwitchPreference.isChecked() != ThreemaApplication.getMasterKey().isProtected()) {
+		if (masterkeySwitchPreference != null && masterkeySwitchPreference.isChecked() != ThreemaApplication.getMasterKey().isProtected()) {
 			masterkeySwitchPreference.setChecked(ThreemaApplication.getMasterKey().isProtected());
 		}
 		setMasterKeyPreferenceText();
 
 		masterkeySwitchPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
-			public boolean onPreferenceChange(Preference preference, Object newValue) {
+			public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
 				boolean newCheckedValue = newValue.equals(true);
 
 				if (((TwoStatePreference) preference).isChecked() != newCheckedValue) {
@@ -312,11 +312,6 @@ public class SettingsSecurityFragment extends ThreemaPreferenceFragment implemen
 				uiLockSwitchPreference.setEnabled(true);
 				break;
 			case PreferenceService.LockingMech_SYSTEM:
-				pinPreference.setEnabled(false);
-				gracePreference.setEnabled(true);
-				uiLockSwitchPreference.setEnabled(true);
-				preferenceService.setPin(null);
-				break;
 			case PreferenceService.LockingMech_BIOMETRIC:
 				pinPreference.setEnabled(false);
 				gracePreference.setEnabled(true);
@@ -357,7 +352,7 @@ public class SettingsSecurityFragment extends ThreemaPreferenceFragment implemen
 		if (resultCode == Activity.RESULT_OK) {
 			switch (requestCode) {
 				case ThreemaActivity.ACTIVITY_ID_CHECK_LOCK:
-					ThreemaApplication.getServiceManager().getScreenLockService().setAuthenticated(true);
+					requireScreenLockService().setAuthenticated(true);
 					onCreateUnlocked();
 					break;
 				case ID_ENABLE_SYSTEM_LOCK:
@@ -389,12 +384,12 @@ public class SettingsSecurityFragment extends ThreemaPreferenceFragment implemen
 
 			// TODO
 			/* show/hide persistent notification */
-			PassphraseService.start(getActivity().getApplicationContext());
+			PassphraseService.start(requireActivity().getApplicationContext());
 		} else {
 			switch (requestCode) {
 				case ThreemaActivity.ACTIVITY_ID_CHECK_LOCK:
-					ThreemaApplication.getServiceManager().getScreenLockService().setAuthenticated(false);
-					getActivity().onBackPressed();
+					requireScreenLockService().setAuthenticated(false);
+					requireActivity().onBackPressed();
 					break;
 				case ThreemaActivity.ACTIVITY_ID_SET_PASSPHRASE:
 					//only switch back on set
@@ -429,7 +424,7 @@ public class SettingsSecurityFragment extends ThreemaPreferenceFragment implemen
 	@TargetApi(Build.VERSION_CODES.M)
 	private void setSystemScreenLock() {
 		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-			KeyguardManager keyguardManager = (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE);
+			KeyguardManager keyguardManager = (KeyguardManager) requireActivity().getSystemService(Context.KEYGUARD_SERVICE);
 			if (keyguardManager.isDeviceSecure()) {
 				BiometricUtil.showUnlockDialog(null, this, true, ID_ENABLE_SYSTEM_LOCK, PreferenceService.LockingMech_SYSTEM);
 			} else {
@@ -501,7 +496,7 @@ public class SettingsSecurityFragment extends ThreemaPreferenceFragment implemen
 	private void startSetPassphraseActivity() {
 		GenericAlertDialog dialog = GenericAlertDialog.newInstance(R.string.warning, getString(R.string.password_remember_warning, getString(R.string.app_name)), R.string.ok, R.string.cancel);
 		dialog.setTargetFragment(this, 0);
-		dialog.show(getFragmentManager(), DIALOG_TAG_PASSWORD_REMINDER_PASSPHRASE);
+		dialog.show(getParentFragmentManager(), DIALOG_TAG_PASSWORD_REMINDER_PASSPHRASE);
 	}
 
 	private void startChangePassphraseActivity() {
@@ -626,16 +621,10 @@ public class SettingsSecurityFragment extends ThreemaPreferenceFragment implemen
 
 	@Override
 	public void onNo(String tag, Object data) {
-		switch (tag) {
-			case DIALOG_TAG_PASSWORD_REMINDER_PASSPHRASE:
-				break;
-			case DIALOG_TAG_PASSWORD_REMINDER_PIN:
-				// workaround to reset dropdown state
-				lockMechanismPreference.setEnabled(false);
-				lockMechanismPreference.setEnabled(true);
-				break;
-			default:
-				break;
+		if (DIALOG_TAG_PASSWORD_REMINDER_PIN.equals(tag)) {
+			// workaround to reset dropdown state
+			lockMechanismPreference.setEnabled(false);
+			lockMechanismPreference.setEnabled(true);
 		}
 	}
 

+ 39 - 24
app/src/main/java/ch/threema/app/preference/SettingsTroubleshootingFragment.java

@@ -91,6 +91,7 @@ import ch.threema.app.utils.MimeUtil;
 import ch.threema.app.utils.PowermanagerUtil;
 import ch.threema.app.utils.PushUtil;
 import ch.threema.app.utils.RuntimeUtil;
+import ch.threema.app.utils.ShareUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.voip.activities.WebRTCDebugActivity;
 import ch.threema.app.webclient.activities.WebDiagnosticsActivity;
@@ -173,7 +174,7 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 
 		threemaPushTwoStatePreference = getPref(getResources().getString(R.string.preferences__threema_push_switch));
 		threemaPushTwoStatePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
-			public boolean onPreferenceChange(Preference preference, Object newValue) {
+			public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
 				boolean newCheckedValue = newValue.equals(true);
 				if (((TwoStatePreference) preference).isChecked() != newCheckedValue) {
 					if (newCheckedValue) {
@@ -203,7 +204,7 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 		messageLogPreference = getPref(getResources().getString(R.string.preferences__message_log_switch));
 		messageLogPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
 			@Override
-			public boolean onPreferenceChange(Preference preference, Object newValue) {
+			public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
 				boolean newCheckedValue = newValue.equals(true);
 
 				DebugLogFileBackend.setEnabled(newCheckedValue);
@@ -212,19 +213,37 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 			}
 		});
 
+		PreferenceCategory loggingCategory = getPref("pref_key_logging");
 		Preference sendLogPreference = getPref(getResources().getString(R.string.preferences__sendlog));
-		sendLogPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
-			@Override
-			public boolean onPreferenceClick(Preference preference) {
-				prepareSendLogfile();
-				return true;
-			}
-		});
+		Preference exportLogPreference = getPref(getResources().getString(R.string.preferences__exportlog));
+
+		// Do not show send log preference on on prem builds
+		if (ConfigUtils.isOnPremBuild()) {
+			loggingCategory.removePreference(sendLogPreference);
+
+			// Show share options
+			exportLogPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+				@Override
+				public boolean onPreferenceClick(@NonNull Preference preference) {
+					ShareUtil.shareLogfile(requireContext(), fileService);
+					return true;
+				}
+			});
+		} else {
+			loggingCategory.removePreference(exportLogPreference);
+			sendLogPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+				@Override
+				public boolean onPreferenceClick(@NonNull Preference preference) {
+					prepareSendLogfile();
+					return true;
+				}
+			});
+		}
 
 		Preference resetPushPreference = getPref(getResources().getString(R.string.preferences__reset_push));
 		resetPushPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
 			@Override
-			public boolean onPreferenceClick(Preference preference) {
+			public boolean onPreferenceClick(@NonNull Preference preference) {
 				if (pushServicesInstalled) {
 					PushUtil.clearPushTokenSentDate(getActivity());
 					PushUtil.enqueuePushTokenUpdate(getContext(), false, true);
@@ -237,7 +256,7 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 		Preference wallpaperDeletePreferences = getPref(getResources().getString(R.string.preferences__remove_wallpapers));
 		wallpaperDeletePreferences.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
 			@Override
-			public boolean onPreferenceClick(Preference preference) {
+			public boolean onPreferenceClick(@NonNull Preference preference) {
 				GenericAlertDialog dialog = GenericAlertDialog.newInstance(R.string.prefs_title_remove_wallpapers,
 					R.string.really_remove_wallpapers,
 					R.string.ok,
@@ -252,7 +271,7 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 		Preference ringtoneResetPreferences = getPref(getResources().getString(R.string.preferences__reset_ringtones));
 		ringtoneResetPreferences.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
 			@Override
-			public boolean onPreferenceClick(Preference preference) {
+			public boolean onPreferenceClick(@NonNull Preference preference) {
 				GenericAlertDialog dialog = GenericAlertDialog.newInstance(R.string.prefs_title_reset_ringtones,
 					R.string.really_reset_ringtones,
 					R.string.ok,
@@ -266,7 +285,7 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 
 		ipv6Preferences = getPref(getResources().getString(R.string.preferences__ipv6_preferred));
 		ipv6Preferences.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
-			public boolean onPreferenceChange(Preference preference, Object newValue) {
+			public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
 
 				boolean newCheckedValue = newValue.equals(true);
 				boolean oldCheckedValue = ((TwoStatePreference) preference).isChecked();
@@ -290,7 +309,7 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 			powerManagerPrefs.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
 				@Override
-				public boolean onPreferenceClick(Preference preference) {
+				public boolean onPreferenceClick(@NonNull Preference preference) {
 					if (PowermanagerUtil.hasPowerManagerOption(SettingsTroubleshootingFragment.this.getActivity())) {
 						GenericAlertDialog dialog = GenericAlertDialog.newInstance(R.string.disable_powermanager_title,
 							String.format(getString(R.string.disable_powermanager_explain), getString(R.string.app_name)),
@@ -311,7 +330,7 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 				backgroundDataPrefs.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
 					@TargetApi(Build.VERSION_CODES.N)
 					@Override
-					public boolean onPreferenceClick(Preference preference) {
+					public boolean onPreferenceClick(@NonNull Preference preference) {
 						Intent intent = new Intent(Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS);
 						intent.setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID));
 
@@ -330,7 +349,7 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 
 			updatePowerManagerPrefs();
 		} else {
-			PreferenceCategory preferenceCategory = (PreferenceCategory) findPreference("pref_key_fix_device");
+			PreferenceCategory preferenceCategory = getPref("pref_key_fix_device");
 			preferenceScreen.removePreference(preferenceCategory);
 		}
 
@@ -341,7 +360,7 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 
 		echoCancelPreference.setSummary(echoCancelArray[echoCancelIndex]);
 		echoCancelPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
-			public boolean onPreferenceChange(Preference preference, Object newValue) {
+			public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
 				preference.setSummary(echoCancelArray[echoCancelValuesArrayList.indexOf(newValue.toString())]);
 				return true;
 			}
@@ -694,13 +713,9 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 
 	@Override
 	public void onNo(String tag, Object data) {
-		switch (tag) {
-			case DIALOG_TAG_IPV6_APP_RESTART:
-				boolean oldValue = (boolean) data;
-				ipv6Preferences.setChecked(oldValue);
-				break;
-			default:
-				break;
+		if (DIALOG_TAG_IPV6_APP_RESTART.equals(tag)) {
+			boolean oldValue = (boolean) data;
+			ipv6Preferences.setChecked(oldValue);
 		}
 	}
 

+ 27 - 15
app/src/main/java/ch/threema/app/preference/ThreemaPreferenceFragment.kt

@@ -27,10 +27,7 @@ import androidx.annotation.XmlRes
 import androidx.preference.Preference
 import androidx.preference.PreferenceFragmentCompat
 import ch.threema.app.ThreemaApplication
-import ch.threema.app.services.FileService
-import ch.threema.app.services.PreferenceService
-import ch.threema.app.services.SynchronizeContactsService
-import ch.threema.app.services.WallpaperService
+import ch.threema.app.services.*
 import ch.threema.app.services.license.LicenseService
 import ch.threema.base.utils.LoggingUtil
 
@@ -88,7 +85,7 @@ abstract class ThreemaPreferenceFragment : PreferenceFragmentCompat() {
     protected fun <T : Preference> getPref(string: String): T = findPreference(string)
             ?: preferenceNotFound(string)
 
-    protected fun getInitialPreferenceService(): PreferenceService {
+    protected fun requirePreferenceService(): PreferenceService {
         ThreemaApplication.getServiceManager()?.preferenceService?.let {
             return it
         }
@@ -96,7 +93,7 @@ abstract class ThreemaPreferenceFragment : PreferenceFragmentCompat() {
         throw IllegalStateException("Could not get preference service")
     }
 
-    protected fun getInitialLicenceService(): LicenseService<*> {
+    protected fun requireLicenceService(): LicenseService<*> {
         ThreemaApplication.getServiceManager()?.licenseService?.let {
             return it
         }
@@ -104,7 +101,7 @@ abstract class ThreemaPreferenceFragment : PreferenceFragmentCompat() {
         throw IllegalStateException("Could not get license service")
     }
 
-    protected fun getInitialWallpaperService(): WallpaperService {
+    protected fun requireWallpaperService(): WallpaperService {
         ThreemaApplication.getServiceManager()?.wallpaperService?.let {
             return it
         }
@@ -112,20 +109,35 @@ abstract class ThreemaPreferenceFragment : PreferenceFragmentCompat() {
         throw IllegalStateException("Could not get wallpaper service")
     }
 
-    protected fun getInitialFileService(): FileService {
-        ThreemaApplication.getServiceManager()?.fileService?.let {
+    protected fun requireSynchronizeContactsService(): SynchronizeContactsService {
+        ThreemaApplication.getServiceManager()?.synchronizeContactsService?.let {
             return it
         }
-        logger.error("Could not get file service")
-        throw IllegalStateException("Could not get file service")
+        logger.error("Could not get synchronize contacts service")
+        throw IllegalStateException("Could not get synchronize contacts service")
     }
 
-    protected fun getInitialSynchronizeContactsService(): SynchronizeContactsService {
-        ThreemaApplication.getServiceManager()?.synchronizeContactsService?.let {
+    protected fun requireContactService(): ContactService {
+        ThreemaApplication.getServiceManager()?.contactService?.let {
             return it
         }
-        logger.error("Could not get synchronize contacts service")
-        throw IllegalStateException("Could not get synchronize contacts service")
+        logger.error("Could not get contact service")
+        throw IllegalStateException("Could not get contact service")
+    }
+
+    protected fun requireHiddenChatListService(): DeadlineListService {
+        ThreemaApplication.getServiceManager()?.hiddenChatsListService?.let {
+            return it
+        }
+        logger.error("Could not get hidden chat list service")
+        throw IllegalStateException("Could not get hidden chat list service")
+    }
+
+    protected fun requireScreenLockService(): SystemScreenLockService {
+        ThreemaApplication.getServiceManager()?.screenLockService?.let {
+            return it
+        }
+        throw IllegalStateException("Could not get screen lock service")
     }
 
     /**

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

@@ -47,6 +47,7 @@ import ch.threema.app.utils.MessageDiskSizeUtil;
 import ch.threema.app.voip.services.VoipStateService;
 import ch.threema.base.utils.LoggingUtil;
 import ch.threema.base.utils.Utils;
+import ch.threema.domain.models.Contact;
 import ch.threema.domain.models.MessageId;
 import ch.threema.domain.protocol.csp.ProtocolDefines;
 import ch.threema.domain.protocol.csp.coders.MessageBox;
@@ -171,7 +172,8 @@ public class MessageProcessor implements MessageProcessorInterface {
 					validationLogger.info("< Nonce: {}", Utils.byteArrayToHexString(boxmsg.getNonce()));
 					validationLogger.info("< Data: {}", Utils.byteArrayToHexString(boxmsg.getBox()));
 
-					byte[] publicKey = contactStore.getPublicKeyForIdentity(boxmsg.getFromIdentity(), true);
+					Contact contact = contactStore.getContactForIdentity(boxmsg.getFromIdentity(), true, true);
+					byte[] publicKey = contact != null ? contact.getPublicKey() : null;
 					if (publicKey != null) {
 						validationLogger.info("< Public key ({}): {}",
 							boxmsg.getFromIdentity(), Utils.byteArrayToHexString(publicKey));
@@ -485,6 +487,6 @@ public class MessageProcessor implements MessageProcessorInterface {
 		/**
 		 * Message has been ignored due to being blocked or invalid.
 		 */
-		IGNORED;
+		IGNORED
 	}
 }

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

@@ -90,6 +90,7 @@ import ch.threema.app.utils.TestUtil;
 import ch.threema.base.ThreemaException;
 import ch.threema.base.utils.Base32;
 import ch.threema.base.utils.LoggingUtil;
+import ch.threema.domain.models.Contact;
 import ch.threema.domain.models.IdentityType;
 import ch.threema.domain.models.VerificationLevel;
 import ch.threema.domain.protocol.ThreemaFeature;
@@ -1296,7 +1297,8 @@ public class ContactServiceImpl implements ContactService {
 		ContactModel newContact;
 
 		try {
-			publicKey = this.contactStore.fetchPublicKeyForIdentity(identity);
+			Contact contact = this.contactStore.fetchPublicKeyForIdentity(identity, true);
+			publicKey = contact != null ? contact.getPublicKey() : null;
 
 			if (publicKey == null) {
 				throw new InvalidEntryException(R.string.connection_error);
@@ -1429,7 +1431,7 @@ public class ContactServiceImpl implements ContactService {
 			return;
 		}
 
-		if (contactStore.getPublicKeyForIdentity(identity, false) == null) {
+		if (contactStore.getContactForIdentity(identity, false, true) == null) {
 			LicenseService.Credentials credentials = this.licenseService.loadCredentials();
 			if ((credentials instanceof UserCredentials)) {
 				try {

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

@@ -988,7 +988,7 @@ public class ConversationServiceImpl implements ConversationService {
 
 				if(conversationModel == null) {
 					conversationModel = new ConversationModel(receiver);
-					if (addToCache && !distributionListModel.isArchived()) {
+					if (addToCache && !distributionListModel.isArchived() && !distributionListModel.isHidden()) {
 						synchronized (conversationCache) {
 							conversationCache.add(conversationModel);
 						}

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

@@ -23,6 +23,7 @@ package ch.threema.app.services;
 
 import java.util.List;
 
+import androidx.annotation.Nullable;
 import ch.threema.app.messagereceiver.DistributionListMessageReceiver;
 import ch.threema.base.ThreemaException;
 import ch.threema.storage.models.ContactModel;
@@ -33,10 +34,13 @@ public interface DistributionListService extends AvatarService<DistributionListM
 	interface DistributionListFilter {
 		boolean sortingByDate();
 		boolean sortingAscending();
+		boolean showHidden();
 	}
 
 	DistributionListModel getById(int id);
-	DistributionListModel createDistributionList(String name, String[] memberIdentities) throws ThreemaException;
+	DistributionListModel createDistributionList(@Nullable String name, String[] memberIdentities) throws ThreemaException;
+	DistributionListModel createDistributionList(@Nullable String name, String[] memberIdentities, boolean isHidden) throws ThreemaException;
+
 	DistributionListModel updateDistributionList(DistributionListModel distributionListModel, String name, String[] memberIdentities) throws ThreemaException;
 
 	boolean addMemberToDistributionList(DistributionListModel distributionListModel, String identity);

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

@@ -75,14 +75,20 @@ public class DistributionListServiceImpl implements DistributionListService {
 	}
 
 	@Override
-	public DistributionListModel createDistributionList(String name, String[] memberIdentities) {
+	public DistributionListModel createDistributionList(@Nullable String name, String[] memberIdentities) {
+		return createDistributionList(name, memberIdentities, false);
+	}
+
+	@Override
+	public DistributionListModel createDistributionList(@Nullable String name, String[] memberIdentities, boolean isHidden) {
 		final DistributionListModel distributionListModel = new DistributionListModel();
 		distributionListModel.setName(name);
 		distributionListModel.setCreatedAt(new Date());
+		distributionListModel.setHidden(isHidden);
 
 		//create
 		this.databaseServiceNew.getDistributionListModelFactory().create(
-				distributionListModel
+			distributionListModel
 		);
 
 

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

@@ -285,6 +285,14 @@ public interface FileService {
 	 */
 	void removeAllAvatars();
 
+	/**
+	 * Save the thumbnail bytes to disk using the file name specified in the supplied AbstractMessageModel
+	 * @param messageModel Message Model used as the source for the file name
+	 * @param thumbnailBytes Byte Array of the thumbnail bitmap
+	 * @throws Exception
+	 */
+	void saveThumbnail(AbstractMessageModel messageModel, byte[] thumbnailBytes) throws Exception;
+
 	/**
 	 * write a thumbnail to disk
 	 */

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

@@ -1119,11 +1119,16 @@ public class FileServiceImpl implements FileService {
 			throw new Exception("Unable to scale thumbnail");
 		}
 
+		saveThumbnail(messageModel, resizedThumbnailBytes);
+	}
+
+	@Override
+	public void saveThumbnail(AbstractMessageModel messageModel, byte[] thumbnailBytes) throws Exception {
 		File thumbnailFile = this.getMessageThumbnail(messageModel);
 		if (thumbnailFile != null) {
 			FileUtil.createNewFileOrLog(thumbnailFile, logger);
 			logger.info("Writing thumbnail...");
-			this.writeFile(resizedThumbnailBytes, thumbnailFile);
+			this.writeFile(thumbnailBytes, thumbnailFile);
 		}
 	}
 

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

@@ -75,6 +75,7 @@ public interface NotificationService {
 	String NOTIFICATION_CHANNEL_CHAT_ID_PREFIX = "ch";
 	String NOTIFICATION_CHANNEL_VOIP_ID_PREFIX = "voip";
 	String NOTIFICATION_CHANNEL_CHAT_UPDATE_ID_PREFIX = "chu";
+	String NOTIFICATION_CHANNEL_REJECT_SERVICE = "reject";
 
 	interface NotificationSchema {
 		boolean vibrate();

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

@@ -64,6 +64,7 @@ import java.util.List;
 import java.util.Map;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.StringRes;
 import androidx.core.app.NotificationCompat;
@@ -111,6 +112,7 @@ import ch.threema.storage.models.ServerMessageModel;
 import ch.threema.storage.models.group.IncomingGroupJoinRequestModel;
 import ch.threema.storage.models.group.OutgoingGroupJoinRequestModel;
 
+import static android.provider.Settings.System.DEFAULT_NOTIFICATION_URI;
 import static androidx.core.app.NotificationCompat.MessagingStyle.MAXIMUM_RETAINED_MESSAGES;
 import static ch.threema.app.ThreemaApplication.WORK_SYNC_NOTIFICATION_ID;
 import static ch.threema.app.voip.services.VoipCallService.EXTRA_ACTIVITY_MODE;
@@ -647,7 +649,7 @@ public class NotificationServiceImpl implements NotificationService {
 									StatusBarNotification[] notifications = notificationManager.getActiveNotifications();
 									for (StatusBarNotification notification : notifications) {
 										if (notification.getId() == newestGroup.getNotificationId()) {
-											NotificationServiceImpl.this.notify(newestGroup.getNotificationId(), builder);
+											NotificationServiceImpl.this.notify(newestGroup.getNotificationId(), builder, null);
 											break;
 										}
 									}
@@ -658,7 +660,7 @@ public class NotificationServiceImpl implements NotificationService {
 						}
 					}, 4000);
 				} else {
-					this.notify(newestGroup.getNotificationId(), builder);
+					this.notify(newestGroup.getNotificationId(), builder, notificationSchema);
 				}
 			} else {
 				createSingleNotification(newestGroup,
@@ -1497,7 +1499,7 @@ public class NotificationServiceImpl implements NotificationService {
 					.setOnlyAlertOnce(false)
 					.setContentIntent(getPendingIntentForActivity(HomeActivity.class));
 
-		this.notify(ThreemaApplication.NEW_MESSAGE_PIN_LOCKED_NOTIFICATION_ID, builder);
+		this.notify(ThreemaApplication.NEW_MESSAGE_PIN_LOCKED_NOTIFICATION_ID, builder, null);
 
 		showIconBadge(0);
 
@@ -1533,7 +1535,7 @@ public class NotificationServiceImpl implements NotificationService {
 					.setOnlyAlertOnce(false)
 					.setContentIntent(getPendingIntentForActivity(HomeActivity.class));
 
-		this.notify(ThreemaApplication.NEW_MESSAGE_LOCKED_NOTIFICATION_ID, builder);
+		this.notify(ThreemaApplication.NEW_MESSAGE_LOCKED_NOTIFICATION_ID, builder, null);
 
 		logger.info("Showing generic notification (master key locked)");
 	}
@@ -1570,7 +1572,7 @@ public class NotificationServiceImpl implements NotificationService {
 					createPendingIntentWithTaskStack(notificationIntent);
 					builder.setContentIntent(pendingIntent);
 
-					this.notify(ThreemaApplication.NETWORK_BLOCKED_NOTIFICATION_ID, builder);
+					this.notify(ThreemaApplication.NETWORK_BLOCKED_NOTIFICATION_ID, builder, null);
 					logger.info("Showing network blocked notification");
 					return;
 				}
@@ -1607,7 +1609,7 @@ public class NotificationServiceImpl implements NotificationService {
 					.setPriority(NotificationCompat.PRIORITY_MAX)
 					.setAutoCancel(true);
 
-		this.notify(ThreemaApplication.SERVER_MESSAGE_NOTIFICATION_ID, builder);
+		this.notify(ThreemaApplication.SERVER_MESSAGE_NOTIFICATION_ID, builder, null);
 	}
 
 
@@ -1639,7 +1641,7 @@ public class NotificationServiceImpl implements NotificationService {
 			builder.addAction(R.drawable.ic_sd_card_black_24dp, context.getString(R.string.check_now), pendingIntent);
 		}
 
-		this.notify(ThreemaApplication.NOT_ENOUGH_DISK_SPACE_NOTIFICATION_ID, builder);
+		this.notify(ThreemaApplication.NOT_ENOUGH_DISK_SPACE_NOTIFICATION_ID, builder, null);
 	}
 
 	private PendingIntent createPendingIntentWithTaskStack(Intent intent) {
@@ -1693,7 +1695,7 @@ public class NotificationServiceImpl implements NotificationService {
 						.setStyle(new NotificationCompat.BigTextStyle().bigText(content))
 						.addAction(R.drawable.ic_refresh_white_24dp, context.getString(R.string.try_again), sendPendingIntent);
 
-			this.notify(ThreemaApplication.UNSENT_MESSAGE_NOTIFICATION_ID, builder);
+			this.notify(ThreemaApplication.UNSENT_MESSAGE_NOTIFICATION_ID, builder, null);
 		} else {
 			this.cancel(ThreemaApplication.UNSENT_MESSAGE_NOTIFICATION_ID);
 		}
@@ -1720,7 +1722,7 @@ public class NotificationServiceImpl implements NotificationService {
 							.setContentText(content)
 							.setStyle(new NotificationCompat.BigTextStyle().bigText(content));
 
-			this.notify(ThreemaApplication.SAFE_FAILED_NOTIFICATION_ID, builder);
+			this.notify(ThreemaApplication.SAFE_FAILED_NOTIFICATION_ID, builder, null);
 		} else {
 			this.cancel(ThreemaApplication.SAFE_FAILED_NOTIFICATION_ID);
 		}
@@ -1738,7 +1740,7 @@ public class NotificationServiceImpl implements NotificationService {
 
 	@Override
 	public void showIdentityStatesSyncProgress() {
-		showSyncProgress(ThreemaApplication.IDENTITY_SYNC_NOTIFICATION_ID, NOTIFICATION_CHANNEL_IDENTITY_SYNC, R.string.synchronize_contact);
+		showSyncProgress(ThreemaApplication.IDENTITY_SYNC_NOTIFICATION_ID, NOTIFICATION_CHANNEL_IDENTITY_SYNC, R.string.synchronizing);
 	}
 
 	@Override
@@ -1764,7 +1766,7 @@ public class NotificationServiceImpl implements NotificationService {
 				.setLocalOnly(true)
 				.setOnlyAlertOnce(true);
 
-		this.notify(notificationId, builder);
+		this.notify(notificationId, builder, null);
 	}
 
 
@@ -1810,7 +1812,7 @@ public class NotificationServiceImpl implements NotificationService {
 							.setPriority(NotificationCompat.PRIORITY_HIGH)
 							.setAutoCancel(true);
 
-			this.notify(ThreemaApplication.NEW_SYNCED_CONTACTS_NOTIFICATION_ID, builder);
+			this.notify(ThreemaApplication.NEW_SYNCED_CONTACTS_NOTIFICATION_ID, builder, null);
 		}
 	}
 
@@ -1820,9 +1822,24 @@ public class NotificationServiceImpl implements NotificationService {
 	 * @param builder
 	 */
 
-	private void notify(int id, NotificationCompat.Builder builder) {
+	private void notify(int id, NotificationCompat.Builder builder, @Nullable NotificationSchema schema) {
 		try {
 			notificationManagerCompat.notify(id, builder.build());
+		} catch (SecurityException e) {
+			// some phones revoke access to selected sound files for notifications after an OS upgrade
+			logger.error("Can't show notification", e);
+			if (schema != null && schema.getSoundUri() != null && !DEFAULT_NOTIFICATION_URI.equals(schema.getSoundUri())) {
+				// create a new schema with default sound
+				NotificationSchemaImpl newSchema = new NotificationSchemaImpl(this.context);
+				newSchema.setSoundUri(DEFAULT_NOTIFICATION_URI);
+				newSchema.setVibrate(schema.vibrate()).setColor(schema.getColor());
+				builder.setChannelId(NotificationBuilderWrapper.init(context, NOTIFICATION_CHANNEL_CHAT, newSchema, false));
+				try {
+					notificationManagerCompat.notify(id, builder.build());
+				} catch (Exception ex) {
+					logger.error("Failed to show fallback notification", ex);
+				}
+			}
 		} catch (Exception e) {
 			// catch FileUriExposedException - see https://commonsware.com/blog/2016/09/07/notifications-sounds-android-7p0-aggravation.html
 			logger.error("Exception", e);
@@ -1936,7 +1953,7 @@ public class NotificationServiceImpl implements NotificationService {
 				.setContentTitle(this.context.getString(R.string.app_name))
 				.setContentText(msg)
 				.setStyle(new NotificationCompat.BigTextStyle().bigText(msg));
-		this.notify(ThreemaApplication.WEB_RESUME_FAILED_NOTIFICATION_ID, builder);
+		this.notify(ThreemaApplication.WEB_RESUME_FAILED_NOTIFICATION_ID, builder, null);
 	}
 
 	@Override
@@ -2004,7 +2021,7 @@ public class NotificationServiceImpl implements NotificationService {
 				.setPriority(NotificationCompat.PRIORITY_HIGH)
 				.setAutoCancel(true);
 
-		this.notify(ThreemaApplication.GROUP_RESPONSE_NOTIFICATION_ID, builder);
+		this.notify(ThreemaApplication.GROUP_RESPONSE_NOTIFICATION_ID, builder, null);
 	}
 
 	@Override
@@ -2068,7 +2085,7 @@ public class NotificationServiceImpl implements NotificationService {
 
 		addGroupLinkActions(notifBuilder, acceptPendingIntent, rejectPendingIntent);
 
-		this.notify(requestIdNonce, notifBuilder);
+		this.notify(requestIdNonce, notifBuilder, null);
 	}
 
 }

+ 6 - 4
app/src/main/java/ch/threema/app/services/PreferenceService.java

@@ -191,7 +191,9 @@ public interface PreferenceService {
 
 	void setTransmittedFeatureLevel(int featureLevel);
 
-	String[] getList(String listName);
+	@NonNull String[] getList(String listName);
+
+	@NonNull String[] getList(String listName, boolean encrypted);
 
 	void setList(String listName, String[] elements);
 
@@ -483,9 +485,6 @@ public interface PreferenceService {
 	void setCameraFlashMode(int flashMode);
 	int getCameraFlashMode();
 
-	void setCameraLensFacing(int lensFacing);
-	int getCameraLensFacing();
-
 	void setPipPosition(int pipPosition);
 	int getPipPosition();
 
@@ -513,4 +512,7 @@ public interface PreferenceService {
 
 	void setAudioPlaybackSpeed(float newSpeed);
 	float getAudioPlaybackSpeed();
+
+	int getMultipleRecipientsTooltipCount();
+	void incrementMultipleRecipientsTooltipCount();
 }

+ 26 - 17
app/src/main/java/ch/threema/app/services/PreferenceServiceImpl.java

@@ -530,15 +530,24 @@ public class PreferenceServiceImpl implements PreferenceService {
 	@Override
 	@NonNull
 	public String[] getList(String listName) {
-		String[] res = this.preferenceStore.getStringArray(listName, true);
-		if (res == null) {
-			res = this.preferenceStore.getStringArray(listName);
-			if (res == null) {
-				return new String[0];
+		return  getList(listName, true);
+	}
+
+	@Override
+	@NonNull
+	public String[] getList(String listName, boolean encrypted) {
+		String[] res = this.preferenceStore.getStringArray(listName, encrypted);
+		if (res == null && encrypted) {
+			// check if we have an old unencrypted identity list - migrate if necessary and return its values
+			if (this.preferenceStore.has(listName)) {
+				res = this.preferenceStore.getStringArray(listName, false);
+				this.preferenceStore.remove(listName, false);
+				if (res != null) {
+					this.preferenceStore.save(listName, res, true);
+				}
 			}
 		}
-
-		return res;
+		return res != null ? res : new String[0];
 	}
 
 	@Override
@@ -1443,16 +1452,6 @@ public class PreferenceServiceImpl implements PreferenceService {
 		return this.preferenceStore.getInt(this.getKeyName(R.string.preferences__camera_flash_mode));
 	}
 
-	@Override
-	public void setCameraLensFacing(int lensFacing) {
-		this.preferenceStore.save(this.getKeyName(R.string.preferences__camera_lens_facing), lensFacing);
-	}
-
-	@Override
-	public int getCameraLensFacing() {
-		return this.preferenceStore.getInt(this.getKeyName(R.string.preferences__camera_lens_facing));
-	}
-
 	@Override
 	public void setPipPosition(int pipPosition) {
 		this.preferenceStore.save(this.getKeyName(R.string.preferences__pip_position), pipPosition);
@@ -1550,4 +1549,14 @@ public class PreferenceServiceImpl implements PreferenceService {
 	public float getAudioPlaybackSpeed() {
 		return this.preferenceStore.getFloat(this.getKeyName(R.string.preferences__audio_playback_speed), 1f);
 	}
+
+	@Override
+	public int getMultipleRecipientsTooltipCount() {
+		return this.preferenceStore.getInt(this.getKeyName(R.string.preferences__tooltip_multi_recipients));
+	}
+
+	@Override
+	public void incrementMultipleRecipientsTooltipCount() {
+		this.preferenceStore.save(this.getKeyName(R.string.preferences__tooltip_multi_recipients), getMultipleRecipientsTooltipCount() + 1);
+	}
 }

+ 57 - 0
app/src/main/java/ch/threema/app/services/systemupdate/SystemUpdateToVersion71.java

@@ -0,0 +1,57 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2021-2022 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;
+
+public class SystemUpdateToVersion71 extends UpdateToVersion implements UpdateSystemService.SystemUpdate {
+	public static final int VERSION = 71;
+	public static final String VERSION_STRING = "version " + VERSION;
+
+	private final SQLiteDatabase sqLiteDatabase;
+
+	public SystemUpdateToVersion71(SQLiteDatabase sqLiteDatabase) {
+		this.sqLiteDatabase = sqLiteDatabase;
+	}
+
+	@Override
+	public boolean runASync() { return true; }
+
+	@Override
+	public boolean runDirectly() throws SQLException {
+		// add isHidden column if not present already
+		String table = "distribution_list";
+		String column = "isHidden";
+
+		if (!this.fieldExist(this.sqLiteDatabase, table, column)) {
+			sqLiteDatabase.rawExecSQL("ALTER TABLE " + table + " ADD COLUMN " + column + " TINYINT DEFAULT 0");
+		}
+		return true;
+	}
+
+	@Override
+	public String getText() { return VERSION_STRING; }
+}

+ 57 - 41
app/src/main/java/ch/threema/app/stores/DatabaseContactStore.java

@@ -74,7 +74,7 @@ public class DatabaseContactStore implements ContactStore {
 	}
 
 	@Override
-	public byte[] getPublicKeyForIdentity(@NonNull String identity, boolean fetch) {
+	public Contact getContactForIdentity(@NonNull String identity, boolean fetch, boolean saveContact) {
 		Contact contact = this.getContactForIdentity(identity);
 
 		if (contact == null) {
@@ -94,7 +94,7 @@ public class DatabaseContactStore implements ContactStore {
 								//try to select again
 								contact = this.getContactForIdentity(identity);
 								if (contact != null) {
-									return contact.getPublicKey();
+									return contact;
 								}
 							}
 						}
@@ -105,7 +105,7 @@ public class DatabaseContactStore implements ContactStore {
 						}
 					}
 
-					return this.fetchPublicKeyForIdentity(identity);
+					return this.fetchPublicKeyForIdentity(identity, saveContact);
 				} catch (Exception e) {
 					logger.error("Exception", e);
 					return null;
@@ -114,62 +114,49 @@ public class DatabaseContactStore implements ContactStore {
 			return null;
 		}
 
-		return contact.getPublicKey();
+		return contact;
 	}
 
 	/**
-	 * Fetch a public key for an identity, create a contact and save it
+	 * Fetch a public key for an identity, create a contact and save it (if requested)
 	 *
 	 * @param identity Identity to add a contact for
+	 * @param saveContact save contact if it does not exist; if false, the contact is added as hidden contact
 	 * @throws ThreemaException if a contact with this identity already exists
 	 *          FileNotFoundException if identity was not found on the server
 	 * @return public key of identity in case of success, null otherwise
 	 */
 	@WorkerThread
-	public @Nullable byte[] fetchPublicKeyForIdentity(@NonNull String identity) throws FileNotFoundException, ThreemaException {
-		APIConnector.FetchIdentityResult result = null;
-		try {
-			Contact contact = this.getContactForIdentity(identity);
-			if(contact != null) {
-				//cannot fetch and save... contact already exists
-				throw new ThreemaException("contact already exists, cannot fetch and save");
-			}
-
-			result = this.apiConnector.fetchIdentity(identity);
-		}
-		catch (FileNotFoundException e) {
-			throw e;
-		} catch (Exception e) {
-			//do nothing
+	public @Nullable Contact fetchPublicKeyForIdentity(@NonNull String identity, boolean saveContact) throws FileNotFoundException, ThreemaException {
+		APIConnector.FetchIdentityResult result = getContactResult(identity);
+		if (result == null || result.publicKey == null) {
 			return null;
 		}
-		byte[] b = result.publicKey;
 
-		if(b != null) {
-			ContactModel contact = new ContactModel(identity, b);
-			contact.setFeatureMask(result.featureMask);
-			contact.setVerificationLevel(VerificationLevel.UNVERIFIED);
-			contact.setDateCreated(new Date());
-			contact.setIdentityType(result.type);
-			switch (result.state) {
-				case IdentityState.ACTIVE:
-					contact.setState(ContactModel.State.ACTIVE);
-					break;
-				case IdentityState.INACTIVE:
-					contact.setState(ContactModel.State.INACTIVE);
-					break;
-				case IdentityState.INVALID:
-					contact.setState(ContactModel.State.INVALID);
-					break;
+		byte[] b = result.publicKey;
 
-			}
+		ContactModel contact = new ContactModel(identity, b);
+		contact.setFeatureMask(result.featureMask);
+		contact.setVerificationLevel(VerificationLevel.UNVERIFIED);
+		contact.setDateCreated(new Date());
+		contact.setIdentityType(result.type);
+		switch (result.state) {
+			case IdentityState.ACTIVE:
+				contact.setState(ContactModel.State.ACTIVE);
+				break;
+			case IdentityState.INACTIVE:
+				contact.setState(ContactModel.State.INACTIVE);
+				break;
+			case IdentityState.INVALID:
+				contact.setState(ContactModel.State.INVALID);
+				break;
+		}
 
+		if (saveContact) {
 			this.addContact(contact);
-
-			return b;
 		}
 
-		return null;
+		return contact;
 	}
 
 	@Override
@@ -191,9 +178,18 @@ public class DatabaseContactStore implements ContactStore {
 
 	@Override
 	public void addContact(@NonNull Contact contact) {
+		addContact(contact, false);
+	}
+
+	@Override
+	public void addContact(@NonNull Contact contact, boolean hide) {
 		ContactModel contactModel = (ContactModel)contact;
 		boolean isUpdate = false;
 
+		if (hide) {
+			contactModel.setIsHidden(true);
+		}
+
 		ContactModelFactory contactModelFactory = this.databaseServiceNew.getContactModelFactory();
 		//get db record
 		ContactModel existingModel = contactModelFactory.getByIdentity(contactModel.getIdentity());
@@ -274,4 +270,24 @@ public class DatabaseContactStore implements ContactStore {
 			}
 		});
 	}
+
+	private APIConnector.FetchIdentityResult getContactResult(@NonNull String identity) throws FileNotFoundException {
+		APIConnector.FetchIdentityResult result;
+		try {
+			Contact contact = this.getContactForIdentity(identity);
+			if(contact != null) {
+				//cannot fetch and save... contact already exists
+				throw new ThreemaException("contact already exists, cannot fetch and save");
+			}
+
+			result = this.apiConnector.fetchIdentity(identity);
+		}
+		catch (FileNotFoundException e) {
+			throw e;
+		} catch (Exception e) {
+			//do nothing
+			return null;
+		}
+		return result;
+	}
 }

+ 10 - 4
app/src/main/java/ch/threema/app/stores/PreferenceStore.java

@@ -54,6 +54,7 @@ import javax.crypto.CipherInputStream;
 import javax.crypto.CipherOutputStream;
 
 import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 import androidx.preference.PreferenceManager;
@@ -136,7 +137,7 @@ public class PreferenceStore implements PreferenceStoreInterface {
 		} else {
 			SharedPreferences.Editor e = this.sharedPreferences.edit();
 			e.putString(key, thing);
-			e.commit();
+			e.apply();
 		}
 
 		this.fireOnChanged(key, thing);
@@ -192,7 +193,7 @@ public class PreferenceStore implements PreferenceStoreInterface {
 	}
 
 	@Override
-	public void save(String key, String[] things, boolean crypt) {
+	public void save(String key, @NonNull String[] things, boolean crypt) {
 		StringBuilder sb = new StringBuilder();
 		for(String s: things) {
 			if(sb.length() > 0) {
@@ -207,7 +208,7 @@ public class PreferenceStore implements PreferenceStoreInterface {
 		} else {
 			SharedPreferences.Editor e = this.sharedPreferences.edit();
 			e.putString(key, sb.toString());
-			e.commit();
+			e.apply();
 		}
 		this.fireOnChanged(key, things);
 	}
@@ -225,7 +226,7 @@ public class PreferenceStore implements PreferenceStoreInterface {
 		} else {
 			SharedPreferences.Editor e = this.sharedPreferences.edit();
 			e.putLong(key, thing);
-			e.commit();
+			e.apply();
 		}
 		this.fireOnChanged(key, thing);
 	}
@@ -698,6 +699,11 @@ public class PreferenceStore implements PreferenceStoreInterface {
 		}
 	}
 
+	@Override
+	public boolean has(String listName) {
+		return this.sharedPreferences.contains(listName);
+	}
+
 	private void fireOnChanged(final String key, final  Object value) {
 		ListenerManager.preferenceListeners.handle(new ListenerManager.HandleListener<PreferenceListener>() {
 			@Override

+ 4 - 1
app/src/main/java/ch/threema/app/stores/PreferenceStoreInterface.java

@@ -32,6 +32,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 public interface PreferenceStoreInterface {
@@ -56,7 +57,7 @@ public interface PreferenceStoreInterface {
 
 	void saveIntegerHashMap(String key, HashMap<Integer, Integer> things);
 
-	void save(String key, String[] things, boolean crypt);
+	void save(String key, @NonNull String[] things, boolean crypt);
 
 	void save(String key, long thing);
 
@@ -141,4 +142,6 @@ public interface PreferenceStoreInterface {
 	Map<String, ?> getAllNonCrypted();
 
 	Set<String> getStringSet(String key, int defaultRes);
+
+	boolean has(String listName);
 }

+ 8 - 1
app/src/main/java/ch/threema/app/stores/PreferenceStoreInterfaceDevNullImpl.java

@@ -33,6 +33,8 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import androidx.annotation.NonNull;
+
 public class PreferenceStoreInterfaceDevNullImpl implements PreferenceStoreInterface {
 	@Override
 	public void remove(String key) {
@@ -83,7 +85,7 @@ public class PreferenceStoreInterfaceDevNullImpl implements PreferenceStoreInter
 	}
 
 	@Override
-	public void save(String key, String[] things, boolean crypt) {
+	public void save(String key, @NonNull String[] things, boolean crypt) {
 
 	}
 
@@ -296,4 +298,9 @@ public class PreferenceStoreInterfaceDevNullImpl implements PreferenceStoreInter
 	public Set<String> getStringSet(String key, int defaultRes) {
 		return new HashSet<String>();
 	}
+
+	@Override
+	public boolean has(String listName) {
+		return false;
+	}
 }

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

@@ -1646,6 +1646,11 @@ public class ThreemaSafeServiceImpl implements ThreemaSafeService {
 			public boolean sortingAscending() {
 				return false;
 			}
+
+			@Override
+			public boolean showHidden() {
+				return false;
+			}
 		})) {
 			distributionlistsArray.put(getDistributionlist(distributionListService, distributionListModel));
 		}

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

@@ -144,7 +144,7 @@ public class ConfigUtils {
 	private static String localeOverride = null;
 	private static Integer primaryColor = null, accentColor = null, miuiVersion = null;
 	private static int emojiStyle = 0;
-	private static Boolean isTablet = null, isBiggerSingleEmojis = null, hasNoMapboxSupport = null;
+	private static Boolean isTablet = null, isBiggerSingleEmojis = null, hasMapLibreSupport = null;
 	private static int preferredThumbnailWidth = -1, preferredAudioMessageWidth = -1;
 
 	private static final float[] NEGATIVE_MATRIX = {
@@ -259,14 +259,14 @@ public class ConfigUtils {
 		return new TLSUpgradeSocketFactoryWrapper(TrustKit.getInstance().getSSLSocketFactory(host));
 	}
 
-	public static boolean hasNoMapboxSupport() {
-		/* Some broken Samsung devices crash on Mapbox initialization due to a compiler bug, see https://issuetracker.google.com/issues/37013676 */
+	public static boolean hasNoMapLibreSupport() {
+		/* Some broken Samsung devices crash on MapLibre initialization due to a compiler bug, see https://issuetracker.google.com/issues/37013676 */
 		/* Device that do not support OCSP stapling cannot use our maps and POI servers */
-		if (hasNoMapboxSupport == null) {
-			hasNoMapboxSupport =
+		if (hasMapLibreSupport == null) {
+			hasMapLibreSupport =
 				Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1 && Build.MANUFACTURER.equalsIgnoreCase("marshall");
 		}
-		return hasNoMapboxSupport;
+		return hasMapLibreSupport;
 	}
 
 	public static boolean isXiaomiDevice() {
@@ -753,6 +753,9 @@ public class ConfigUtils {
 				case "zh-rTW":
 					conf.locale = new Locale("zh", "TW");
 					break;
+				case "be-rBY":
+					conf.locale = new Locale("be", "BY");
+					break;
 				default:
 					conf.locale = new Locale(confLanguage);
 					break;

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

@@ -23,6 +23,7 @@ package ch.threema.app.utils;
 
 import android.Manifest;
 import android.annotation.TargetApi;
+import android.app.Notification;
 import android.app.NotificationManager;
 import android.content.Context;
 import android.content.SharedPreferences;
@@ -38,6 +39,8 @@ import java.util.Calendar;
 import java.util.Set;
 
 import androidx.annotation.Nullable;
+import androidx.core.app.NotificationChannelCompat;
+import androidx.core.app.NotificationChannelGroupCompat;
 import androidx.core.app.NotificationManagerCompat;
 import androidx.core.content.ContextCompat;
 import androidx.preference.PreferenceManager;
@@ -221,15 +224,42 @@ public class DNDUtil {
 	}
 
 	/**
-	 * Check if the contact specified in messageReceiver is muted in the system. "Starred" contacts may override the global DND setting in "priority" mode
-	 * and should be signalled.
+	 * Check if the contact specified in messageReceiver is muted in the system.
+	 *
+	 * Notifications in Android are one big mess!
+	 *
+	 * "Starred" contacts may override the global DND setting in "priority" mode
+	 * and should be signalled (needless to say this applies to contacts that are linked with a system contact only).
+	 * Also, notification channels can be configured to override the system's DND setting. Accordingly we should signal them too.
+	 * Additionally, notifications may be blocked on a notification channel group level. In that case, ignore any DND override settings
+	 *
+	 * I dare not imagine if this will work on Xiaomi devices with their hideous tinkering on top of the normal Android notification system...
+	 *
 	 * @param messageReceiver A MessageReceiver representing a ContactModel
+	 * @param notification The notification
 	 * @param notificationManager
 	 * @param notificationManagerCompat
-	 * @return false if the contact is not muted in the system and a ringtone should be played, false otherwise
+	 * @return true if no ringtone should ne played, false otherwise
 	 */
-	public boolean isSystemMuted(MessageReceiver messageReceiver, NotificationManager notificationManager, NotificationManagerCompat notificationManagerCompat) {
+	public boolean isSystemMuted(MessageReceiver messageReceiver, @Nullable Notification notification, NotificationManager notificationManager, NotificationManagerCompat notificationManagerCompat) {
 		boolean isSystemMuted = !notificationManagerCompat.areNotificationsEnabled();
+		boolean canBypassDND = false;
+
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && notification != null) {
+			NotificationChannelCompat notificationChannelCompat = notificationManagerCompat.getNotificationChannelCompat(notification.getChannelId());
+			if (notificationChannelCompat != null) {
+				String groupName = notificationChannelCompat.getGroup();
+				if (groupName != null) {
+					NotificationChannelGroupCompat notificationChannelGroupCompat = notificationManagerCompat.getNotificationChannelGroupCompat(groupName);
+					if (notificationChannelGroupCompat != null) {
+						if (notificationChannelGroupCompat.isBlocked()) {
+							return true;
+						}
+					}
+				}
+				canBypassDND = notificationChannelCompat.canBypassDnd();
+			}
+		}
 
 		if (messageReceiver instanceof ContactMessageReceiver) {
 			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -246,6 +276,7 @@ public class DNDUtil {
 				}
 			}
 		}
-		return isSystemMuted;
+
+		return isSystemMuted && !canBypassDND;
 	}
 }

+ 30 - 0
app/src/main/java/ch/threema/app/utils/ShareUtil.java

@@ -23,12 +23,19 @@ package ch.threema.app.utils;
 
 import android.content.Context;
 import android.content.Intent;
+import android.net.Uri;
+import android.widget.Toast;
 
+import java.io.File;
+
+import androidx.annotation.NonNull;
 import androidx.core.app.ActivityCompat;
 import ch.threema.app.BuildConfig;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
+import ch.threema.app.services.FileService;
 import ch.threema.app.services.UserService;
+import ch.threema.logging.backend.DebugLogFileBackend;
 import ch.threema.storage.models.ContactModel;
 
 import static androidx.core.content.ContextCompat.startActivity;
@@ -58,4 +65,27 @@ public class ShareUtil {
 		shareIntent.putExtra(Intent.EXTRA_TEXT, text);
 		startActivity(context, Intent.createChooser(shareIntent, context.getString(R.string.share_via)), null);
 	}
+
+	/**
+	 * Share the logfile with another application.
+	 *
+	 * @param context     the context
+	 * @param fileService the file service
+	 */
+	public static void shareLogfile(@NonNull Context context, @NonNull FileService fileService) {
+		File zipFile = DebugLogFileBackend.getZipFile(fileService);
+		if (zipFile == null) {
+			Toast.makeText(context, context.getResources().getString(R.string.try_again), Toast.LENGTH_SHORT).show();
+			return;
+		}
+
+		Uri uriToLogfile = fileService.getShareFileUri(zipFile, "debug_log.zip");
+
+		Intent shareIntent = new Intent(Intent.ACTION_SEND);
+		shareIntent.setType("application/zip");
+		shareIntent.putExtra(Intent.EXTRA_STREAM, uriToLogfile);
+		shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+		startActivity(context, Intent.createChooser(shareIntent, context.getResources().getString(R.string.share_via)), null);
+	}
 }

+ 7 - 0
app/src/main/java/ch/threema/app/utils/ShortcutUtil.java

@@ -55,6 +55,8 @@ import ch.threema.app.ThreemaApplication;
 import ch.threema.app.activities.ComposeMessageActivity;
 import ch.threema.app.activities.MainActivity;
 import ch.threema.app.activities.RecipientListActivity;
+import ch.threema.app.backuprestore.csv.BackupService;
+import ch.threema.app.backuprestore.csv.RestoreService;
 import ch.threema.app.messagereceiver.ContactMessageReceiver;
 import ch.threema.app.messagereceiver.DistributionListMessageReceiver;
 import ch.threema.app.messagereceiver.GroupMessageReceiver;
@@ -295,6 +297,11 @@ public final class ShortcutUtil {
 			return;
 		}
 
+		if (BackupService.isRunning() || RestoreService.isRunning()) {
+			logger.info("Backup / Restore is running. Exiting");
+			return;
+		}
+
 		final ConversationService.Filter filter = new ConversationService.Filter() {
 			@Override
 			public boolean onlyUnread() {

+ 3 - 1
app/src/main/java/ch/threema/app/voicemessage/VoiceRecorderActivity.java

@@ -794,6 +794,7 @@ public class VoiceRecorderActivity extends AppCompatActivity implements DefaultL
 			case STATE_RECORDING:
 				activateSensors(true);
 				pauseButton.setImageResource(R.drawable.ic_pause);
+				pauseButton.clearColorFilter();
 				pauseButton.setVisibility(supportsPauseResume() ? View.VISIBLE : View.INVISIBLE);
 				pauseButton.setContentDescription(getString(R.string.pause));
 				playButton.setImageResource(R.drawable.ic_stop);
@@ -808,6 +809,7 @@ public class VoiceRecorderActivity extends AppCompatActivity implements DefaultL
 			case STATE_PAUSED:
 				activateSensors(false);
 				pauseButton.setImageResource(R.drawable.ic_record);
+				pauseButton.setColorFilter(getResources().getColor(R.color.material_red), PorterDuff.Mode.SRC_IN);
 				pauseButton.setVisibility(supportsPauseResume() ? View.VISIBLE : View.INVISIBLE);
 				pauseButton.setContentDescription(getString(R.string.continue_recording));
 				playButton.setImageResource(R.drawable.ic_stop);
@@ -815,7 +817,7 @@ public class VoiceRecorderActivity extends AppCompatActivity implements DefaultL
 				recordImage.setVisibility(View.INVISIBLE);
 				stopBlinking();
 				stopTimer();
-				inhibitPinLock(false);
+				inhibitPinLock(true);
 				break;
 			case STATE_PLAYING_PAUSED:
 				activateSensors(false);

+ 10 - 6
app/src/main/java/ch/threema/app/voip/VoipBluetoothManager.java

@@ -487,13 +487,17 @@ public class VoipBluetoothManager {
 			// hack / bold assumption - fallback to list of bonded devices
 			if (bluetoothHeadset != null) {
 				if (bluetoothAdapter != null && BluetoothProfile.STATE_CONNECTED == bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)) {
-					Set<BluetoothDevice> bondedDevices = bluetoothAdapter.getBondedDevices();
-					for (BluetoothDevice bondedDevice : bondedDevices) {
-						if (bondedDevice.getType() == DEVICE_TYPE_CLASSIC &&
-							bondedDevice.getBluetoothClass().hasService(BluetoothClass.Service.AUDIO) &&
-							bondedDevice.getBondState() == BOND_BONDED) {
-							devices.add(bondedDevice);
+					try {
+						Set<BluetoothDevice> bondedDevices = bluetoothAdapter.getBondedDevices();
+						for (BluetoothDevice bondedDevice : bondedDevices) {
+							if (bondedDevice.getType() == DEVICE_TYPE_CLASSIC &&
+								bondedDevice.getBluetoothClass().hasService(BluetoothClass.Service.AUDIO) &&
+								bondedDevice.getBondState() == BOND_BONDED) {
+								devices.add(bondedDevice);
+							}
 						}
+					} catch (SecurityException ex) {
+						logger.error("Unable to get bonded devices", ex);
 					}
 				}
 			}

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

@@ -1813,7 +1813,7 @@ public class CallActivity extends ThreemaActivity implements
 			rejectIntent.putExtra(VoipCallService.EXTRA_CONTACT_IDENTITY, contact.getIdentity());
 			rejectIntent.putExtra(VoipCallService.EXTRA_CALL_ID, callId);
 			rejectIntent.putExtra(CallRejectService.EXTRA_REJECT_REASON, reason);
-			CallRejectService.enqueueWork(this, rejectIntent);
+			ContextCompat.startForegroundService(this, rejectIntent);
 		} else if (this.activityMode == MODE_ACTIVE_CALL) {
 			VoipUtil.sendVoipCommand(CallActivity.this, VoipCallService.class, VoipCallService.ACTION_HANGUP);
 			setResult(RESULT_CANCELED);

+ 74 - 13
app/src/main/java/ch/threema/app/voip/services/CallRejectService.java

@@ -21,21 +21,28 @@
 
 package ch.threema.app.voip.services;
 
-import android.content.Context;
+import android.annotation.TargetApi;
+import android.app.IntentService;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
 import android.content.Intent;
+import android.os.Build;
 
 import org.slf4j.Logger;
 
-import androidx.annotation.NonNull;
-import androidx.core.app.FixedJobIntentService;
+import androidx.annotation.Nullable;
+import androidx.core.app.NotificationCompat;
+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.NotificationService;
 import ch.threema.app.voip.activities.CallActivity;
 import ch.threema.app.voip.util.VoipUtil;
 import ch.threema.base.ThreemaException;
-import ch.threema.domain.protocol.csp.messages.voip.VoipCallAnswerData;
 import ch.threema.base.utils.LoggingUtil;
+import ch.threema.domain.protocol.csp.messages.voip.VoipCallAnswerData;
 import ch.threema.storage.models.ContactModel;
 
 import static ch.threema.app.voip.services.VoipCallService.EXTRA_CALL_ID;
@@ -44,23 +51,29 @@ import static ch.threema.app.voip.services.VoipCallService.EXTRA_CONTACT_IDENTIT
 /**
  * A small intent service that rejects an incoming call.
  */
-public class CallRejectService extends FixedJobIntentService {
-	private static final Logger logger = LoggingUtil.getThreemaLogger("CallRejectService");
+public class CallRejectService extends IntentService {
+	private static final String name = "CallRejectService";
+	private static final Logger logger = LoggingUtil.getThreemaLogger(name);
+	private static final int CALL_REJECT_NOTIFICATION_ID = 27349;
+
 	public static final String EXTRA_REJECT_REASON = "REJECT_REASON";
 
 	private VoipStateService voipStateService = null;
 	private ContactService contactService = null;
 
-	public static final int JOB_ID = 344339;
-
-	public static void enqueueWork(Context context, Intent work) {
-		logger.info("enqueueWork");
-		enqueueWork(context, CallRejectService.class, JOB_ID, work);
+	public CallRejectService() {
+		super(name);
 	}
 
 	@Override
-	protected void onHandleWork(@NonNull Intent intent) {
-		logger.info("onHandleWork");
+	protected void onHandleIntent(@Nullable Intent intent) {
+		logger.info("onHandleIntent");
+
+		// Ignore null intents
+		if (intent == null) {
+			logger.debug("Empty Intent");
+			return;
+		}
 
 		// Intent parameters
 		final String contactIdentity = intent.getStringExtra(EXTRA_CONTACT_IDENTITY);
@@ -108,4 +121,52 @@ public class CallRejectService extends FixedJobIntentService {
 		// Clear the candidates cache
 		voipStateService.clearCandidatesCache(contactIdentity);
 	}
+
+	@Override
+	public void onCreate() {
+		logger.info("onCreate");
+
+		super.onCreate();
+
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+			startForeground(CALL_REJECT_NOTIFICATION_ID, getForegroundNotification());
+		}
+	}
+
+	@Override
+	public void onDestroy() {
+		logger.info("onDestroy");
+
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+			stopForeground(true);
+		}
+
+		super.onDestroy();
+	}
+
+	@TargetApi(Build.VERSION_CODES.O)
+	private Notification getForegroundNotification() {
+		createChannel();
+		NotificationCompat.Builder builder =
+			(new NotificationCompat.Builder(this.getApplicationContext(), NotificationService.NOTIFICATION_CHANNEL_REJECT_SERVICE))
+				.setGroup(NotificationService.NOTIFICATION_CHANNEL_REJECT_SERVICE)
+				.setContentTitle(getString(R.string.voip_reject_channel_name))
+				.setSmallIcon(R.drawable.ic_call_grey600_24dp)
+				.setPriority(NotificationCompat.PRIORITY_MIN)
+				.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
+
+        return builder.build();
+	}
+
+	@TargetApi(Build.VERSION_CODES.O)
+	private void createChannel() {
+		NotificationChannel channel = new NotificationChannel(
+			NotificationService.NOTIFICATION_CHANNEL_REJECT_SERVICE,
+			getResources().getString(R.string.voip_reject_channel_name),
+			NotificationManager.IMPORTANCE_LOW);
+		channel.enableVibration(false);
+		channel.enableLights(false);
+		channel.setShowBadge(false);
+		getSystemService(NotificationManager.class).createNotificationChannel(channel);
+	}
 }

+ 21 - 22
app/src/main/java/ch/threema/app/voip/services/VoipStateService.java

@@ -60,6 +60,7 @@ import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 import androidx.core.app.NotificationCompat;
 import androidx.core.app.NotificationManagerCompat;
+import androidx.core.content.ContextCompat;
 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
@@ -82,7 +83,6 @@ import ch.threema.app.voip.CallStateSnapshot;
 import ch.threema.app.voip.Config;
 import ch.threema.app.voip.activities.CallActivity;
 import ch.threema.app.voip.managers.VoipListenerManager;
-import ch.threema.app.voip.receivers.CallRejectReceiver;
 import ch.threema.app.voip.receivers.VoipMediaButtonReceiver;
 import ch.threema.app.voip.util.VoipUtil;
 import ch.threema.base.ThreemaException;
@@ -477,7 +477,7 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 	 * Create a new reject intent for the specified call ID / identity.
 	 */
 	public static Intent createRejectIntent(long callId, @NonNull String identity, byte rejectReason) {
-		final Intent intent = new Intent(getAppContext(), CallRejectReceiver.class);
+		final Intent intent = new Intent(getAppContext(), CallRejectService.class);
 		intent.putExtra(EXTRA_CALL_ID, callId);
 		intent.putExtra(EXTRA_CONTACT_IDENTITY, identity);
 		intent.putExtra(EXTRA_IS_INITIATOR, false);
@@ -612,17 +612,15 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 		this.acceptIntent = accept;
 
 		// If the call is rejected, start the CallRejectService
-		final Intent rejectIntent = this.createRejectIntent(
+		final Intent rejectIntent = createRejectIntent(
 			callId,
 			msg.getFromIdentity(),
 			VoipCallAnswerData.RejectReason.REJECTED
 		);
-		final PendingIntent reject = PendingIntent.getBroadcast(
-				this.appContext,
-				-IdUtil.getTempId(contact),
-				rejectIntent,
-				PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT
-		);
+
+		final PendingIntent reject = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ?
+			PendingIntent.getForegroundService(this.appContext, -IdUtil.getTempId(contact), rejectIntent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT) :
+			PendingIntent.getService(this.appContext, -IdUtil.getTempId(contact), rejectIntent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
 
 		final ContactMessageReceiver messageReceiver = this.contactService.createReceiver(contact);
 
@@ -631,11 +629,11 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 		// Set state to RINGING
 		this.setStateRinging(callId);
 
-		// Play ringtone
-		this.playRingtone(messageReceiver, isMuted);
-
 		// Show call notification
-		this.showNotification(contact, callId, accept, reject, msg, callOfferData, isMuted);
+		final Notification notification = this.showNotification(contact, accept, reject, msg, isMuted);
+
+		// Play ringtone
+		this.playRingtone(notification, messageReceiver, isMuted);
 
 		// Send "ringing" message to caller
 		try {
@@ -678,7 +676,7 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 					msg.getFromIdentity(),
 					VoipCallAnswerData.RejectReason.TIMEOUT
 				);
-				CallRejectService.enqueueWork(appContext, rejectIntent1);
+				ContextCompat.startForegroundService(appContext, rejectIntent1);
 			}
 		};
 		(new Handler(Looper.getMainLooper()))
@@ -1383,19 +1381,19 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 	/**
 	 * Show a call notification.
 	 */
+	@Nullable
 	@WorkerThread
-	private void showNotification(
+	private Notification showNotification(
 		@NonNull ContactModel contact,
-		long callId,
 		@Nullable PendingIntent accept,
 		@NonNull PendingIntent reject,
 		final VoipCallOfferMessage msg,
-		final VoipCallOfferData offerData,
 		boolean isMuted
 	) {
 		final long timestamp = System.currentTimeMillis();
 		final Bitmap avatar = this.contactService.getAvatar(contact, false);
 		final PendingIntent inCallPendingIntent = createLaunchPendingIntent(contact.getIdentity(), msg);
+		Notification notification = null;
 
 		if (notificationManagerCompat.areNotificationsEnabled()) {
 			final NotificationCompat.Builder nbuilder = new NotificationBuilderWrapper(this.appContext, NOTIFICATION_CHANNEL_CALL, isMuted);
@@ -1455,7 +1453,7 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 					.addAction(R.drawable.ic_call_grey600_24dp, acceptString, accept != null ? accept : inCallPendingIntent);
 
 			// Build notification
-			final Notification notification = nbuilder.build();
+			notification = nbuilder.build();
 
 			// Set flags
 			notification.flags |= NotificationCompat.FLAG_INSISTENT | NotificationCompat.FLAG_NO_CLEAR | NotificationCompat.FLAG_ONGOING_EVENT;
@@ -1464,7 +1462,6 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 				this.notificationManagerCompat.notify(contact.getIdentity(), INCOMING_CALL_NOTIFICATION_ID, notification);
 				this.callNotificationTags.add(contact.getIdentity());
 			}
-
 		} else {
 			// notifications disabled in system settings - fire inCall pending intent to show CallActivity
 			try {
@@ -1476,9 +1473,11 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 
 		// register screen off receiver
 		registerScreenOffReceiver();
+
+		return notification;
 	}
 
-	private void playRingtone(MessageReceiver messageReceiver, boolean isMuted) {
+	private void playRingtone(@Nullable Notification notification, MessageReceiver messageReceiver, boolean isMuted) {
 		final Uri ringtoneUri = this.ringtoneService.getVoiceCallRingtone(messageReceiver.getUniqueIdString());
 
 		if (ringtoneUri != null) {
@@ -1486,9 +1485,9 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 				stopRingtone();
 			}
 
-			boolean isSystemMuted = DNDUtil.getInstance().isSystemMuted(messageReceiver, notificationManager, notificationManagerCompat);
+			boolean isSystemMuted = DNDUtil.getInstance().isSystemMuted(messageReceiver, notification, notificationManager, notificationManagerCompat);
 
-			if (!isMuted && !isSystemMuted) {
+			if (!isMuted && !isSystemMuted ) {
 				audioManager.requestAudioFocus(this, AudioManager.STREAM_RING, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
 				ringtonePlayer = new MediaPlayerStateWrapper();
 				ringtonePlayer.setStateListener(new MediaPlayerStateWrapper.StateListener() {

+ 3 - 2
app/src/main/java/ch/threema/logging/backend/DebugLogFileBackend.java

@@ -41,12 +41,13 @@ import java.util.Date;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import ch.threema.app.BuildConfig;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.services.FileService;
 import ch.threema.app.utils.ZipUtil;
 import ch.threema.app.utils.executor.HandlerExecutor;
-import ch.threema.logging.LogLevel;
 import ch.threema.base.utils.LoggingUtil;
+import ch.threema.logging.LogLevel;
 import java8.util.concurrent.CompletableFuture;
 
 /**
@@ -60,7 +61,7 @@ import java8.util.concurrent.CompletableFuture;
  */
 public class DebugLogFileBackend implements LogBackend {
 	// Constants
-	private static final String TAG = "3ma";
+	private static final String TAG = BuildConfig.LOG_TAG;
 	private static final String LOGFILE_NAME = "debug_log.txt";
 
 	// Static variables

+ 3 - 2
app/src/main/java/ch/threema/logging/backend/LogcatBackend.java

@@ -27,14 +27,15 @@ import org.slf4j.helpers.MessageFormatter;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import ch.threema.logging.LogLevel;
+import ch.threema.app.BuildConfig;
 import ch.threema.base.utils.LoggingUtil;
+import ch.threema.logging.LogLevel;
 
 /**
  * A logging backend that logs to the ADB logcat.
  */
 public class LogcatBackend implements LogBackend {
-	private final static String TAG = "3ma";
+	private final static String TAG = BuildConfig.LOG_TAG;
 	private final @LogLevel int minLogLevel;
 
 	// For tags starting with these prefixes, the package path is stripped

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

@@ -99,6 +99,7 @@ import ch.threema.app.services.systemupdate.SystemUpdateToVersion68;
 import ch.threema.app.services.systemupdate.SystemUpdateToVersion69;
 import ch.threema.app.services.systemupdate.SystemUpdateToVersion7;
 import ch.threema.app.services.systemupdate.SystemUpdateToVersion70;
+import ch.threema.app.services.systemupdate.SystemUpdateToVersion71;
 import ch.threema.app.services.systemupdate.SystemUpdateToVersion8;
 import ch.threema.app.services.systemupdate.SystemUpdateToVersion9;
 import ch.threema.app.utils.FileUtil;
@@ -133,7 +134,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 = SystemUpdateToVersion70.VERSION;
+	private static final int DATABASE_VERSION = SystemUpdateToVersion71.VERSION;
 
 	private final Context context;
 	private final String key;
@@ -631,6 +632,9 @@ public class DatabaseServiceNew extends SQLiteOpenHelper {
 		if (oldVersion < SystemUpdateToVersion70.VERSION) {
 			this.updateSystemService.addUpdate(new SystemUpdateToVersion70(sqLiteDatabase));
 		}
+		if (oldVersion < SystemUpdateToVersion71.VERSION) {
+			this.updateSystemService.addUpdate(new SystemUpdateToVersion71(sqLiteDatabase));
+		}
 	}
 
 	public void executeNull() throws SQLiteException {

+ 15 - 5
app/src/main/java/ch/threema/storage/factories/DistributionListModelFactory.java

@@ -99,7 +99,8 @@ public class DistributionListModelFactory extends ModelFactory {
 							.setId(cursorHelper.getInt(DistributionListModel.COLUMN_ID))
 							.setName(cursorHelper.getString(DistributionListModel.COLUMN_NAME))
 							.setCreatedAt(cursorHelper.getDateByString(DistributionListModel.COLUMN_CREATED_AT))
-							.setArchived(cursorHelper.getBoolean(DistributionListModel.COLUMN_IS_ARCHIVED));
+							.setArchived(cursorHelper.getBoolean(DistributionListModel.COLUMN_IS_ARCHIVED))
+							.setHidden(cursorHelper.getBoolean(DistributionListModel.COLUMN_IS_HIDDEN));
 
 					return false;
 				}
@@ -147,6 +148,7 @@ public class DistributionListModelFactory extends ModelFactory {
 		contentValues.put(DistributionListModel.COLUMN_NAME, distributionListModel.getName());
 		contentValues.put(DistributionListModel.COLUMN_CREATED_AT, distributionListModel.getCreatedAt() != null ? CursorHelper.dateAsStringFormat.get().format(distributionListModel.getCreatedAt()) : null);
 		contentValues.put(DistributionListModel.COLUMN_IS_ARCHIVED, distributionListModel.isArchived());
+		contentValues.put(DistributionListModel.COLUMN_IS_HIDDEN, distributionListModel.isHidden());
 
 		return contentValues;
 	}
@@ -210,23 +212,31 @@ public class DistributionListModelFactory extends ModelFactory {
 
 		//sort by id!
 		String orderBy = null;
+		// do not show hidden distribution lists by default
+		String where = DistributionListModel.COLUMN_IS_HIDDEN + " !=1";
 
-		if(filter != null) {
-			if(!filter.sortingByDate()) {
+		if (filter != null) {
+			if (!filter.sortingByDate()) {
 				orderBy = DistributionListModel.COLUMN_CREATED_AT + " " + (filter.sortingAscending() ? "ASC" : "DESC");
 			}
+			if (filter.showHidden()) {
+				where = null;
+			}
+		}
+
+		if (where != null) {
+			queryBuilder.appendWhere(where);
 		}
 
 		return convert(
 				queryBuilder,
 				orderBy);
-
 	}
 
 	@Override
 	public String[] getStatements() {
 		return new String[]{
-				"CREATE TABLE `distribution_list` (`id` INTEGER PRIMARY KEY AUTOINCREMENT , `name` VARCHAR , `createdAt` VARCHAR, `isArchived` TINYINT DEFAULT 0 );"
+				"CREATE TABLE `distribution_list` (`id` INTEGER PRIMARY KEY AUTOINCREMENT , `name` VARCHAR , `createdAt` VARCHAR, `isArchived` TINYINT DEFAULT 0 , `isHidden` TINYINT DEFAULT 0 );"
 		};
 	}
 }

+ 14 - 3
app/src/main/java/ch/threema/storage/models/DistributionListModel.java

@@ -24,6 +24,7 @@ package ch.threema.storage.models;
 import java.util.Date;
 import java.util.Objects;
 
+import androidx.annotation.Nullable;
 import ch.threema.base.utils.Utils;
 
 public class DistributionListModel implements ReceiverModel {
@@ -34,18 +35,19 @@ public class DistributionListModel implements ReceiverModel {
 	public static final String COLUMN_NAME = "name";
 	public static final String COLUMN_CREATED_AT = "createdAt";
 	public static final String COLUMN_IS_ARCHIVED = "isArchived"; /* whether this distribution list has been archived by user */
+	public static final String COLUMN_IS_HIDDEN = "isHidden"; /* whether this distribution list is hidden from view */
 
 	private int id;
 	private String name;
 	private Date createdAt;
-	private boolean isArchived;
+	private boolean isArchived, isHidden;
 
 	// dummy class
-	public String getName() {
+	public @Nullable String getName() {
 		return this.name;
 	}
 
-	public DistributionListModel setName(String name) {
+	public DistributionListModel setName(@Nullable String name) {
 		this.name = Utils.truncateUTF8String(name, DISTRIBUTIONLIST_NAME_MAX_LENGTH_BYTES);
 		return this;
 	}
@@ -77,6 +79,15 @@ public class DistributionListModel implements ReceiverModel {
 		return this;
 	}
 
+	public boolean isHidden() {
+		return isHidden;
+	}
+
+	public DistributionListModel setHidden(boolean hidden) {
+		isHidden = hidden;
+		return this;
+	}
+
 	@Override
 	public boolean equals(Object o) {
 		if (this == o) return true;

+ 5 - 3
app/src/main/res/layout/activity_unlock_masterkey.xml

@@ -53,9 +53,10 @@
 				android:layout_width="wrap_content"
 				android:layout_height="wrap_content"
 				android:layout_gravity="center_vertical"
+				android:layout_marginBottom="3dp"
 				android:layout_marginRight="8dp"
 				app:srcCompat="@drawable/ic_key_outline"
-				android:tint="?attr/textColorSecondary"/>
+				app:tint="?attr/textColorSecondary" />
 
 			<com.google.android.material.textfield.TextInputLayout
 				android:id="@+id/passphrase_layout"
@@ -86,15 +87,16 @@
 				android:layout_width="36dp"
 				android:layout_height="36dp"
 				android:layout_marginLeft="8dp"
+				android:layout_marginBottom="3dp"
 				android:layout_gravity="center_vertical"
 				android:background="@drawable/circle_transparent"
 				android:contentDescription="@string/edit"
 				android:rotation="180"
 				android:scaleType="center"
-				android:tint="?attr/colorAccent"
 				app:srcCompat="@drawable/ic_arrow_left"
 				android:enabled="false"
-				android:clickable="false"/>
+				android:clickable="false"
+				app:tint="?attr/colorAccent" />
 
 		</LinearLayout>
 

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor