瀏覽代碼

Version 4.6

Threema 4 年之前
父節點
當前提交
0b6543eafe
共有 100 個文件被更改,包括 2839 次插入1457 次删除
  1. 4 4
      README.md
  2. 2 1
      THANKS.md
  3. 200 87
      app/build.gradle
  4. 1 1
      app/proguard-project.txt
  5. 10 4
      app/src/androidTest/java/ch/threema/app/backuprestore/csv/BackupServiceTest.java
  6. 74 0
      app/src/androidTest/java/ch/threema/app/processors/MessageAckProcessorTest.java
  7. 244 0
      app/src/androidTest/java/ch/threema/app/processors/MessageProcessorTest.java
  8. 308 0
      app/src/androidTest/java/ch/threema/app/service/GroupInviteServiceTest.java
  9. 71 0
      app/src/androidTest/java/ch/threema/app/testutils/CaptureLogcatOnTestFailureRule.java
  10. 65 2
      app/src/androidTest/java/ch/threema/app/testutils/TestHelpers.java
  11. 40 0
      app/src/androidTest/java/ch/threema/app/testutils/ThreemaAssert.java
  12. 2 2
      app/src/androidTest/java/ch/threema/app/utils/BackgroundErrorNotificationTest.java
  13. 1 1
      app/src/androidTest/java/ch/threema/app/voip/VoipStatusMessageTest.java
  14. 1 1
      app/src/androidTest/java/ch/threema/app/webclient/converter/MessageTest.java
  15. 1 1
      app/src/google_services_based/java/ch/threema/app/push/PushRegistrationWorker.java
  16. 1 1
      app/src/google_services_based/java/ch/threema/app/push/PushService.java
  17. 1 1
      app/src/google_services_based/java/ch/threema/app/wearable/WearableHandler.java
  18. 1 1
      app/src/hms_services_based/java/ch/threema/app/push/PushRegistrationWorker.java
  19. 1 1
      app/src/hms_services_based/java/ch/threema/app/push/PushService.java
  20. 33 14
      app/src/main/AndroidManifest.xml
  21. 6 29
      app/src/main/java/ch/threema/app/BuildFlavor.java
  22. 1 4
      app/src/main/java/ch/threema/app/NamedFileProvider.java
  23. 0 147
      app/src/main/java/ch/threema/app/RecipientChooserTargetService.java
  24. 105 53
      app/src/main/java/ch/threema/app/ThreemaApplication.java
  25. 19 6
      app/src/main/java/ch/threema/app/actions/LocationMessageSendAction.java
  26. 12 5
      app/src/main/java/ch/threema/app/actions/TextMessageSendAction.java
  27. 37 94
      app/src/main/java/ch/threema/app/activities/AddContactActivity.java
  28. 45 35
      app/src/main/java/ch/threema/app/activities/AppLinksActivity.java
  29. 6 1
      app/src/main/java/ch/threema/app/activities/ContactDetailActivity.java
  30. 8 14
      app/src/main/java/ch/threema/app/activities/DirectoryActivity.java
  31. 135 63
      app/src/main/java/ch/threema/app/activities/EnterSerialActivity.java
  32. 2 2
      app/src/main/java/ch/threema/app/activities/ExportIDActivity.java
  33. 1 6
      app/src/main/java/ch/threema/app/activities/ExportIDResultActivity.java
  34. 3 2
      app/src/main/java/ch/threema/app/activities/GroupAddActivity.java
  35. 155 109
      app/src/main/java/ch/threema/app/activities/GroupDetailActivity.java
  36. 38 9
      app/src/main/java/ch/threema/app/activities/HomeActivity.java
  37. 1 1
      app/src/main/java/ch/threema/app/activities/IdentityListActivity.java
  38. 3 6
      app/src/main/java/ch/threema/app/activities/ImagePaintActivity.java
  39. 11 18
      app/src/main/java/ch/threema/app/activities/MapActivity.java
  40. 1 1
      app/src/main/java/ch/threema/app/activities/MediaGalleryActivity.java
  41. 1 31
      app/src/main/java/ch/threema/app/activities/MediaViewerActivity.java
  42. 1 1
      app/src/main/java/ch/threema/app/activities/PinLockActivity.java
  43. 4 9
      app/src/main/java/ch/threema/app/activities/QRCodeZoomActivity.java
  44. 30 11
      app/src/main/java/ch/threema/app/activities/RecipientListBaseActivity.java
  45. 18 42
      app/src/main/java/ch/threema/app/activities/SendMediaActivity.java
  46. 1 1
      app/src/main/java/ch/threema/app/activities/StopPassphraseServiceActivity.java
  47. 2 10
      app/src/main/java/ch/threema/app/activities/TextChatBubbleActivity.java
  48. 1 0
      app/src/main/java/ch/threema/app/activities/ThreemaActivity.java
  49. 4 4
      app/src/main/java/ch/threema/app/activities/ThreemaToolbarActivity.java
  50. 2 11
      app/src/main/java/ch/threema/app/activities/WhatsNew2Activity.java
  51. 5 11
      app/src/main/java/ch/threema/app/activities/WhatsNewActivity.java
  52. 53 9
      app/src/main/java/ch/threema/app/activities/ballot/BallotMatrixActivity.java
  53. 5 4
      app/src/main/java/ch/threema/app/activities/ballot/BallotOverviewActivity.java
  54. 1 1
      app/src/main/java/ch/threema/app/activities/ballot/BallotWizardActivity.java
  55. 338 0
      app/src/main/java/ch/threema/app/activities/wizard/WizardBackupRestoreActivity.java
  56. 7 7
      app/src/main/java/ch/threema/app/activities/wizard/WizardBaseActivity.java
  57. 2 0
      app/src/main/java/ch/threema/app/activities/wizard/WizardFingerPrintActivity.java
  58. 7 5
      app/src/main/java/ch/threema/app/activities/wizard/WizardIDRestoreActivity.java
  59. 18 83
      app/src/main/java/ch/threema/app/activities/wizard/WizardIntroActivity.java
  60. 32 262
      app/src/main/java/ch/threema/app/activities/wizard/WizardSafeRestoreActivity.java
  61. 1 1
      app/src/main/java/ch/threema/app/adapters/ComposeMessageAdapter.java
  62. 12 10
      app/src/main/java/ch/threema/app/adapters/ContactDetailAdapter.java
  63. 52 9
      app/src/main/java/ch/threema/app/adapters/ContactListAdapter.java
  64. 6 6
      app/src/main/java/ch/threema/app/adapters/DirectoryAdapter.java
  65. 121 20
      app/src/main/java/ch/threema/app/adapters/GroupDetailAdapter.java
  66. 2 2
      app/src/main/java/ch/threema/app/adapters/MessageListAdapter.java
  67. 1 1
      app/src/main/java/ch/threema/app/adapters/decorators/AnimGifChatAdapterDecorator.java
  68. 6 5
      app/src/main/java/ch/threema/app/adapters/decorators/BallotChatAdapterDecorator.java
  69. 6 13
      app/src/main/java/ch/threema/app/adapters/decorators/ChatAdapterDecorator.java
  70. 1 1
      app/src/main/java/ch/threema/app/adapters/decorators/FileChatAdapterDecorator.java
  71. 3 3
      app/src/main/java/ch/threema/app/adapters/decorators/TextChatAdapterDecorator.java
  72. 1 3
      app/src/main/java/ch/threema/app/adapters/decorators/VideoChatAdapterDecorator.java
  73. 3 0
      app/src/main/java/ch/threema/app/archive/ArchiveActivity.java
  74. 3 3
      app/src/main/java/ch/threema/app/asynctasks/AddContactAsyncTask.java
  75. 8 0
      app/src/main/java/ch/threema/app/asynctasks/DeleteDistributionListAsyncTask.java
  76. 1 1
      app/src/main/java/ch/threema/app/asynctasks/DeleteIdentityAsyncTask.java
  77. 8 0
      app/src/main/java/ch/threema/app/asynctasks/DeleteMyGroupAsyncTask.java
  78. 56 12
      app/src/main/java/ch/threema/app/asynctasks/EmptyChatAsyncTask.java
  79. 8 0
      app/src/main/java/ch/threema/app/asynctasks/LeaveGroupAsyncTask.java
  80. 17 5
      app/src/main/java/ch/threema/app/backuprestore/csv/BackupService.java
  81. 1 1
      app/src/main/java/ch/threema/app/backuprestore/csv/Helper.java
  82. 19 9
      app/src/main/java/ch/threema/app/backuprestore/csv/RestoreService.java
  83. 2 1
      app/src/main/java/ch/threema/app/backuprestore/csv/RestoreSettings.java
  84. 3 0
      app/src/main/java/ch/threema/app/backuprestore/csv/Tags.java
  85. 8 15
      app/src/main/java/ch/threema/app/camera/CameraActivity.java
  86. 0 3
      app/src/main/java/ch/threema/app/camera/CameraFragment.java
  87. 1 5
      app/src/main/java/ch/threema/app/camera/CameraUtil.java
  88. 1 3
      app/src/main/java/ch/threema/app/camera/VideoEditView.java
  89. 1 4
      app/src/main/java/ch/threema/app/camera/ZoomView.java
  90. 2 2
      app/src/main/java/ch/threema/app/dialogs/ContactEditDialog.java
  91. 29 12
      app/src/main/java/ch/threema/app/dialogs/MessageDetailDialog.java
  92. 1 1
      app/src/main/java/ch/threema/app/dialogs/NewContactDialog.java
  93. 32 4
      app/src/main/java/ch/threema/app/dialogs/PasswordEntryDialog.java
  94. 109 0
      app/src/main/java/ch/threema/app/dialogs/PublicKeyDialog.java
  95. 4 2
      app/src/main/java/ch/threema/app/dialogs/SMSVerificationDialog.java
  96. 73 56
      app/src/main/java/ch/threema/app/dialogs/SelectorDialog.java
  97. 8 2
      app/src/main/java/ch/threema/app/dialogs/ShowOnceDialog.java
  98. 2 5
      app/src/main/java/ch/threema/app/dialogs/SimpleStringAlertDialog.java
  99. 2 7
      app/src/main/java/ch/threema/app/dialogs/TextEntryDialog.java
  100. 43 1
      app/src/main/java/ch/threema/app/dialogs/WizardDialog.java

+ 4 - 4
README.md

@@ -103,7 +103,7 @@ Threema employee.
 
 
 ## <a name="build-variants"></a>Build Variants
 ## <a name="build-variants"></a>Build Variants
 
 
-There are currently six product flavors:
+There are currently nine product flavors:
 
 
 | Flavor              | Description                                   | License Checks |
 | Flavor              | Description                                   | License Checks |
 | ------------------- | --------------------------------------------- | -------------- |
 | ------------------- | --------------------------------------------- | -------------- |
@@ -194,10 +194,10 @@ to understand the design concepts.
 
 
 Code related to the core functionality (e.g., connecting to the chat server,
 Code related to the core functionality (e.g., connecting to the chat server,
 encrypting messages, etc.) can be found in the
 encrypting messages, etc.) can be found in the
-`app/src/main/java/ch/threema/client/` directory.
+`domain/src/main/java/ch/threema/` directory.
 
 
-The code of the actual Android app is mainly located in the
-`app/src/main/java/ch/threema/app/` directory.
+The code of the actual Android app is located in the
+`app/src/main/java/ch/threema/` directory.
 
 
 
 
 ## <a name="contributions"></a>Contributions
 ## <a name="contributions"></a>Contributions

+ 2 - 1
THANKS.md

@@ -3,13 +3,14 @@
 The following people have contributed to Threema for Android through GitHub:
 The following people have contributed to Threema for Android through GitHub:
 
 
 * [das-g] ([#2])
 * [das-g] ([#2])
-* [oemel09] ([#3], [#7])
+* [oemel09] ([#3], [#7], [#8])
 
 
 Thank you!
 Thank you!
 
 
 [das-g]: https://github.com/das-g
 [das-g]: https://github.com/das-g
 [oemel09]: https://github.com/oemel09
 [oemel09]: https://github.com/oemel09
 
 
+[#8]: https://github.com/threema-ch/threema-android/pull/8
 [#2]: https://github.com/threema-ch/threema-android/pull/2
 [#2]: https://github.com/threema-ch/threema-android/pull/2
 [#3]: https://github.com/threema-ch/threema-android/pull/3
 [#3]: https://github.com/threema-ch/threema-android/pull/3
 [#7]: https://github.com/threema-ch/threema-android/pull/7
 [#7]: https://github.com/threema-ch/threema-android/pull/7

+ 200 - 87
app/build.gradle

@@ -1,8 +1,17 @@
 plugins {
 plugins {
-    id "org.sonarqube" version "3.0"
+    id 'org.sonarqube'
 }
 }
 
 
 apply plugin: 'com.android.application'
 apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+// rules to allow test running with a real android jar implementation of URI, Uri.Builder for group link encoding
+apply plugin: 'de.mobilej.unmock'
+unMock {
+    keepStartingWith "android.net.Uri"
+    keep "android.net.Uri"
+    keepAndRename "java.nio.charset.Charsets" to "xjava.nio.charset.Charsets"
+    keepStartingWith "libcore."
+}
 
 
 // only apply the plugin if we are dealing with a AppGallery build
 // only apply the plugin if we are dealing with a AppGallery build
 if (getGradle().getStartParameter().getTaskRequests().toString().contains("Hms")) {
 if (getGradle().getStartParameter().getTaskRequests().toString().contains("Hms")) {
@@ -11,7 +20,7 @@ if (getGradle().getStartParameter().getTaskRequests().toString().contains("Hms")
 }
 }
 
 
 // version codes
 // version codes
-def app_version = "4.59"
+def app_version = "4.6"
 def beta_suffix = "" // with leading dash
 def beta_suffix = "" // with leading dash
 
 
 /**
 /**
@@ -71,33 +80,37 @@ def keystores = [
     debug: findKeystore("debug"),
     debug: findKeystore("debug"),
     release: findKeystore("threema"),
     release: findKeystore("threema"),
     hms_release: findKeystore("threema_hms"),
     hms_release: findKeystore("threema_hms"),
+    onprem_release: findKeystore("onprem"),
+    red_release: findKeystore("red"),
 ]
 ]
 
 
 android {
 android {
-    // NOTE: When adjusting compileSdkVersion or buildToolsVersion,
-    //       make sure to adjust them in `scripts/Dockerfile` as well!
-    compileSdkVersion 29
-    buildToolsVersion '29.0.3'
+    // NOTE: When adjusting compileSdkVersion, buildToolsVersion or ndkVersion,
+    //       make sure to adjust them in `scripts/Dockerfile` and
+    //       `.gitlab-ci.yml` as well!
+    compileSdkVersion 30
+    buildToolsVersion '30.0.3'
+    ndkVersion '21.1.6352462'
 
 
     defaultConfig {
     defaultConfig {
-        minSdkVersion 19
+        minSdkVersion 21
         //noinspection OldTargetApi
         //noinspection OldTargetApi
-        targetSdkVersion 29
+        targetSdkVersion 30
         vectorDrawables.useSupportLibrary = true
         vectorDrawables.useSupportLibrary = true
         applicationId "ch.threema.app"
         applicationId "ch.threema.app"
         testApplicationId 'ch.threema.app.test'
         testApplicationId 'ch.threema.app.test'
-        versionCode 699
+        versionCode 705
         versionName "${app_version}${beta_suffix}"
         versionName "${app_version}${beta_suffix}"
         resValue "string", "app_name", "Threema"
         resValue "string", "app_name", "Threema"
         // package name used for sync adapter
         // package name used for sync adapter
         resValue "string", "package_name", applicationId
         resValue "string", "package_name", applicationId
         resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.profile"
         resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.profile"
         resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.call"
         resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.call"
-        resValue "integer", "max_group_size", "256"
-        resValue "string", "shop_download_filename", "Threema-update.apk"
+        buildConfigField "int", "MAX_GROUP_SIZE", "256"
         buildConfigField "String", "CHAT_SERVER_PREFIX", "\"g-\""
         buildConfigField "String", "CHAT_SERVER_PREFIX", "\"g-\""
-        buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"ds.\""
+        buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"ds.g-\""
         buildConfigField "String", "CHAT_SERVER_SUFFIX", "\".0.threema.ch\""
         buildConfigField "String", "CHAT_SERVER_SUFFIX", "\".0.threema.ch\""
+        buildConfigField "int[]", "CHAT_SERVER_PORTS", "{5222, 443}"
         buildConfigField "String", "MEDIA_PATH", "\"Threema\""
         buildConfigField "String", "MEDIA_PATH", "\"Threema\""
         buildConfigField "boolean", "CHAT_SERVER_GROUPS", "true"
         buildConfigField "boolean", "CHAT_SERVER_GROUPS", "true"
         buildConfigField "boolean", "DISABLE_CERT_PINNING", "false"
         buildConfigField "boolean", "DISABLE_CERT_PINNING", "false"
@@ -105,18 +118,35 @@ android {
         buildConfigField "byte[]", "SERVER_PUBKEY", "new byte[] {(byte) 0x45, (byte) 0x0b, (byte) 0x97, (byte) 0x57, (byte) 0x35, (byte) 0x27, (byte) 0x9f, (byte) 0xde, (byte) 0xcb, (byte) 0x33, (byte) 0x13, (byte) 0x64, (byte) 0x8f, (byte) 0x5f, (byte) 0xc6, (byte) 0xee, (byte) 0x9f, (byte) 0xf4, (byte) 0x36, (byte) 0x0e, (byte) 0xa9, (byte) 0x2a, (byte) 0x8c, (byte) 0x17, (byte) 0x51, (byte) 0xc6, (byte) 0x61, (byte) 0xe4, (byte) 0xc0, (byte) 0xd8, (byte) 0xc9, (byte) 0x09 }"
         buildConfigField "byte[]", "SERVER_PUBKEY", "new byte[] {(byte) 0x45, (byte) 0x0b, (byte) 0x97, (byte) 0x57, (byte) 0x35, (byte) 0x27, (byte) 0x9f, (byte) 0xde, (byte) 0xcb, (byte) 0x33, (byte) 0x13, (byte) 0x64, (byte) 0x8f, (byte) 0x5f, (byte) 0xc6, (byte) 0xee, (byte) 0x9f, (byte) 0xf4, (byte) 0x36, (byte) 0x0e, (byte) 0xa9, (byte) 0x2a, (byte) 0x8c, (byte) 0x17, (byte) 0x51, (byte) 0xc6, (byte) 0x61, (byte) 0xe4, (byte) 0xc0, (byte) 0xd8, (byte) 0xc9, (byte) 0x09 }"
         buildConfigField "byte[]", "SERVER_PUBKEY_ALT", "new byte[] {(byte) 0xda, (byte) 0x7c, (byte) 0x73, (byte) 0x79, (byte) 0x8f, (byte) 0x97, (byte) 0xd5, (byte) 0x87, (byte) 0xc3, (byte) 0xa2, (byte) 0x5e, (byte) 0xbe, (byte) 0x0a, (byte) 0x91, (byte) 0x41, (byte) 0x7f, (byte) 0x76, (byte) 0xdb, (byte) 0xcc, (byte) 0xcd, (byte) 0xda, (byte) 0x29, (byte) 0x30, (byte) 0xe6, (byte) 0xa9, (byte) 0x09, (byte) 0x0a, (byte) 0xf6, (byte) 0x2e, (byte) 0xba, (byte) 0x6f, (byte) 0x15 }"
         buildConfigField "byte[]", "SERVER_PUBKEY_ALT", "new byte[] {(byte) 0xda, (byte) 0x7c, (byte) 0x73, (byte) 0x79, (byte) 0x8f, (byte) 0x97, (byte) 0xd5, (byte) 0x87, (byte) 0xc3, (byte) 0xa2, (byte) 0x5e, (byte) 0xbe, (byte) 0x0a, (byte) 0x91, (byte) 0x41, (byte) 0x7f, (byte) 0x76, (byte) 0xdb, (byte) 0xcc, (byte) 0xcd, (byte) 0xda, (byte) 0x29, (byte) 0x30, (byte) 0xe6, (byte) 0xa9, (byte) 0x09, (byte) 0x0a, (byte) 0xf6, (byte) 0x2e, (byte) 0xba, (byte) 0x6f, (byte) 0x15 }"
         buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
         buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
+        buildConfigField "String", "DIRECTORY_SERVER_URL", "\"https://apip.threema.ch/\""
+        buildConfigField "String", "DIRECTORY_SERVER_IPV6_URL", "\"https://ds-apip.threema.ch/\""
+        buildConfigField "String", "WORK_SERVER_URL", "null"
+        buildConfigField "String", "WORK_SERVER_IPV6_URL", "null"
+        buildConfigField "String", "BLOB_SERVER_DOWNLOAD_URL", "\"https://blobp-{blobIdPrefix}.threema.ch/{blobId}\""
+        buildConfigField "String", "BLOB_SERVER_DOWNLOAD_IPV6_URL", "\"https://ds-blobp-{blobIdPrefix}.threema.ch/{blobId}\""
+        buildConfigField "String", "BLOB_SERVER_DONE_URL", "\"https://blobp-{blobIdPrefix}.threema.ch/{blobId}/done\""
+        buildConfigField "String", "BLOB_SERVER_DONE_IPV6_URL", "\"https://ds-blobp-{blobIdPrefix}.threema.ch/{blobId}/done\""
+        buildConfigField "String", "BLOB_SERVER_UPLOAD_URL", "\"https://blobp-upload.threema.ch/upload\""
+        buildConfigField "String", "BLOB_SERVER_UPLOAD_IPV6_URL", "\"https://ds-blobp-upload.threema.ch/upload\""
+        buildConfigField "String", "AVATAR_FETCH_URL", "\"https://avatar.threema.ch/\""
+        buildConfigField "String", "SAFE_SERVER_URL", "\"https://safe-%h.threema.ch/\""
+        buildConfigField "String", "ONPREM_ID_PREFIX", "\"O\""
+
+        buildConfigField "String[]", "ONPREM_CONFIG_TRUSTED_PUBLIC_KEYS", "null"
         buildConfigField "boolean", "SEND_CONSUMED_DELIVERY_RECEIPTS", "false"
         buildConfigField "boolean", "SEND_CONSUMED_DELIVERY_RECEIPTS", "false"
 
 
         // config fields for action URLs / deep links
         // config fields for action URLs / deep links
         buildConfigField "String", "uriScheme", "\"threema\""
         buildConfigField "String", "uriScheme", "\"threema\""
         buildConfigField "String", "actionUrl", "\"go.threema.ch\""
         buildConfigField "String", "actionUrl", "\"go.threema.ch\""
         buildConfigField "String", "contactActionUrl", "\"threema.id\""
         buildConfigField "String", "contactActionUrl", "\"threema.id\""
+        buildConfigField "String", "groupLinkActionUrl", "\"threema.group\""
 
 
         // duplicated for manifest
         // duplicated for manifest
         manifestPlaceholders = [
         manifestPlaceholders = [
             uriScheme: "threema",
             uriScheme: "threema",
-            actionUrl: "go.threema.ch",
-            contactActionUrl: "threema.id"
+            contactActionUrl: "threema.id",
+            groupLinkActionUrl: "threema.group",
+            actionUrl: "go.threema.ch"
         ]
         ]
 
 
         ndk {
         ndk {
@@ -149,146 +179,170 @@ android {
 
 
     flavorDimensions "default"
     flavorDimensions "default"
     productFlavors {
     productFlavors {
-
-
         none { }
         none { }
-        store_google {
-            resValue "string", "shop_download_filename", ""
+        store_google { }
+        store_threema {
+            resValue "string", "shop_download_filename", "Threema-update.apk"
         }
         }
-        store_threema { }
         store_google_work {
         store_google_work {
             versionName "${app_version}k${beta_suffix}"
             versionName "${app_version}k${beta_suffix}"
             applicationId "ch.threema.app.work"
             applicationId "ch.threema.app.work"
             testApplicationId 'ch.threema.app.work.test'
             testApplicationId 'ch.threema.app.work.test'
-            resValue "string", "package_name", applicationId
             resValue "string", "app_name", "Threema Work"
             resValue "string", "app_name", "Threema Work"
+
             resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.work.profile"
             resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.work.profile"
             resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.work.call"
             resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.work.call"
-            resValue "integer", "max_group_size", "256"
-            resValue "string", "shop_download_filename", ""
             buildConfigField "String", "CHAT_SERVER_PREFIX", "\"w-\""
             buildConfigField "String", "CHAT_SERVER_PREFIX", "\"w-\""
-            buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"ds.\""
-            buildConfigField "String", "CHAT_SERVER_SUFFIX", "\".0.threema.ch\""
+            buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"ds.w-\""
             buildConfigField "String", "MEDIA_PATH", "\"ThreemaWork\""
             buildConfigField "String", "MEDIA_PATH", "\"ThreemaWork\""
-            buildConfigField "boolean", "CHAT_SERVER_GROUPS", "true"
+            buildConfigField "String", "WORK_SERVER_URL", "\"https://apip-work.threema.ch/\""
+            buildConfigField "String", "WORK_SERVER_IPV6_URL", "\"https://ds-apip-work.threema.ch/\""
 
 
             // config fields for action URLs / deep links
             // config fields for action URLs / deep links
             buildConfigField "String", "uriScheme", "\"threemawork\""
             buildConfigField "String", "uriScheme", "\"threemawork\""
             buildConfigField "String", "actionUrl", "\"work.threema.ch\""
             buildConfigField "String", "actionUrl", "\"work.threema.ch\""
-            buildConfigField "String", "contactActionUrl", "\"threema.id\""
 
 
             manifestPlaceholders = [
             manifestPlaceholders = [
                 uriScheme: "threemawork",
                 uriScheme: "threemawork",
                 actionUrl: "work.threema.ch",
                 actionUrl: "work.threema.ch",
-                contactActionUrl: "threema.id"
             ]
             ]
         }
         }
         sandbox {
         sandbox {
             applicationId "ch.threema.app.sandbox"
             applicationId "ch.threema.app.sandbox"
             testApplicationId 'ch.threema.app.sandbox.test'
             testApplicationId 'ch.threema.app.sandbox.test'
-
-            resValue "string", "package_name", applicationId
             resValue "string", "app_name", "Threema Sandbox"
             resValue "string", "app_name", "Threema Sandbox"
 
 
             buildConfigField "String", "MEDIA_PATH", "\"ThreemaSandbox\""
             buildConfigField "String", "MEDIA_PATH", "\"ThreemaSandbox\""
             buildConfigField "String", "CHAT_SERVER_SUFFIX", "\".0.test.threema.ch\""
             buildConfigField "String", "CHAT_SERVER_SUFFIX", "\".0.test.threema.ch\""
             buildConfigField "byte[]", "SERVER_PUBKEY", "new byte[] {(byte) 0x5a, (byte) 0x98, (byte) 0xf2, (byte) 0x3d, (byte) 0xe6, (byte) 0x56, (byte) 0x05, (byte) 0xd0, (byte) 0x50, (byte) 0xdc, (byte) 0x00, (byte) 0x64, (byte) 0xbe, (byte) 0x07, (byte) 0xdd, (byte) 0xdd, (byte) 0x81, (byte) 0x1d, (byte) 0xa1, (byte) 0x16, (byte) 0xa5, (byte) 0x43, (byte) 0xce, (byte) 0x43, (byte) 0xaa, (byte) 0x26, (byte) 0x87, (byte) 0xd1, (byte) 0x9f, (byte) 0x20, (byte) 0xaf, (byte) 0x3c }"
             buildConfigField "byte[]", "SERVER_PUBKEY", "new byte[] {(byte) 0x5a, (byte) 0x98, (byte) 0xf2, (byte) 0x3d, (byte) 0xe6, (byte) 0x56, (byte) 0x05, (byte) 0xd0, (byte) 0x50, (byte) 0xdc, (byte) 0x00, (byte) 0x64, (byte) 0xbe, (byte) 0x07, (byte) 0xdd, (byte) 0xdd, (byte) 0x81, (byte) 0x1d, (byte) 0xa1, (byte) 0x16, (byte) 0xa5, (byte) 0x43, (byte) 0xce, (byte) 0x43, (byte) 0xaa, (byte) 0x26, (byte) 0x87, (byte) 0xd1, (byte) 0x9f, (byte) 0x20, (byte) 0xaf, (byte) 0x3c }"
             buildConfigField "byte[]", "SERVER_PUBKEY_ALT", "new byte[] {(byte) 0x5a, (byte) 0x98, (byte) 0xf2, (byte) 0x3d, (byte) 0xe6, (byte) 0x56, (byte) 0x05, (byte) 0xd0, (byte) 0x50, (byte) 0xdc, (byte) 0x00, (byte) 0x64, (byte) 0xbe, (byte) 0x07, (byte) 0xdd, (byte) 0xdd, (byte) 0x81, (byte) 0x1d, (byte) 0xa1, (byte) 0x16, (byte) 0xa5, (byte) 0x43, (byte) 0xce, (byte) 0x43, (byte) 0xaa, (byte) 0x26, (byte) 0x87, (byte) 0xd1, (byte) 0x9f, (byte) 0x20, (byte) 0xaf, (byte) 0x3c }"
             buildConfigField "byte[]", "SERVER_PUBKEY_ALT", "new byte[] {(byte) 0x5a, (byte) 0x98, (byte) 0xf2, (byte) 0x3d, (byte) 0xe6, (byte) 0x56, (byte) 0x05, (byte) 0xd0, (byte) 0x50, (byte) 0xdc, (byte) 0x00, (byte) 0x64, (byte) 0xbe, (byte) 0x07, (byte) 0xdd, (byte) 0xdd, (byte) 0x81, (byte) 0x1d, (byte) 0xa1, (byte) 0x16, (byte) 0xa5, (byte) 0x43, (byte) 0xce, (byte) 0x43, (byte) 0xaa, (byte) 0x26, (byte) 0x87, (byte) 0xd1, (byte) 0x9f, (byte) 0x20, (byte) 0xaf, (byte) 0x3c }"
+            buildConfigField "String", "DIRECTORY_SERVER_URL", "\"https://apip.test.threema.ch/\""
+            buildConfigField "String", "DIRECTORY_SERVER_IPV6_URL", "\"https://ds-apip.test.threema.ch/\""
+            buildConfigField "String", "AVATAR_FETCH_URL", "\"https://avatar.test.threema.ch/\""
         }
         }
         sandbox_work {
         sandbox_work {
             versionName "${app_version}k${beta_suffix}"
             versionName "${app_version}k${beta_suffix}"
             applicationId "ch.threema.app.sandbox.work"
             applicationId "ch.threema.app.sandbox.work"
             testApplicationId 'ch.threema.app.sandbox.work.test'
             testApplicationId 'ch.threema.app.sandbox.work.test'
-
-            resValue "string", "package_name", applicationId
             resValue "string", "app_name", "Threema Sandbox Work"
             resValue "string", "app_name", "Threema Sandbox Work"
+
             resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.work.profile"
             resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.work.profile"
             resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.work.call"
             resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.work.call"
-            resValue "integer", "max_group_size", "256"
-            resValue "string", "shop_download_filename", ""
-
             buildConfigField "String", "CHAT_SERVER_PREFIX", "\"w-\""
             buildConfigField "String", "CHAT_SERVER_PREFIX", "\"w-\""
-            buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"ds.\""
-            buildConfigField "String", "MEDIA_PATH", "\"ThreemaWorkSandbox\""
-            buildConfigField "boolean", "CHAT_SERVER_GROUPS", "true"
-
+            buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"ds.w-\""
             buildConfigField "String", "CHAT_SERVER_SUFFIX", "\".0.test.threema.ch\""
             buildConfigField "String", "CHAT_SERVER_SUFFIX", "\".0.test.threema.ch\""
+            buildConfigField "String", "MEDIA_PATH", "\"ThreemaWorkSandbox\""
             buildConfigField "byte[]", "SERVER_PUBKEY", "new byte[] {(byte) 0x5a, (byte) 0x98, (byte) 0xf2, (byte) 0x3d, (byte) 0xe6, (byte) 0x56, (byte) 0x05, (byte) 0xd0, (byte) 0x50, (byte) 0xdc, (byte) 0x00, (byte) 0x64, (byte) 0xbe, (byte) 0x07, (byte) 0xdd, (byte) 0xdd, (byte) 0x81, (byte) 0x1d, (byte) 0xa1, (byte) 0x16, (byte) 0xa5, (byte) 0x43, (byte) 0xce, (byte) 0x43, (byte) 0xaa, (byte) 0x26, (byte) 0x87, (byte) 0xd1, (byte) 0x9f, (byte) 0x20, (byte) 0xaf, (byte) 0x3c }"
             buildConfigField "byte[]", "SERVER_PUBKEY", "new byte[] {(byte) 0x5a, (byte) 0x98, (byte) 0xf2, (byte) 0x3d, (byte) 0xe6, (byte) 0x56, (byte) 0x05, (byte) 0xd0, (byte) 0x50, (byte) 0xdc, (byte) 0x00, (byte) 0x64, (byte) 0xbe, (byte) 0x07, (byte) 0xdd, (byte) 0xdd, (byte) 0x81, (byte) 0x1d, (byte) 0xa1, (byte) 0x16, (byte) 0xa5, (byte) 0x43, (byte) 0xce, (byte) 0x43, (byte) 0xaa, (byte) 0x26, (byte) 0x87, (byte) 0xd1, (byte) 0x9f, (byte) 0x20, (byte) 0xaf, (byte) 0x3c }"
             buildConfigField "byte[]", "SERVER_PUBKEY_ALT", "new byte[] {(byte) 0x5a, (byte) 0x98, (byte) 0xf2, (byte) 0x3d, (byte) 0xe6, (byte) 0x56, (byte) 0x05, (byte) 0xd0, (byte) 0x50, (byte) 0xdc, (byte) 0x00, (byte) 0x64, (byte) 0xbe, (byte) 0x07, (byte) 0xdd, (byte) 0xdd, (byte) 0x81, (byte) 0x1d, (byte) 0xa1, (byte) 0x16, (byte) 0xa5, (byte) 0x43, (byte) 0xce, (byte) 0x43, (byte) 0xaa, (byte) 0x26, (byte) 0x87, (byte) 0xd1, (byte) 0x9f, (byte) 0x20, (byte) 0xaf, (byte) 0x3c }"
             buildConfigField "byte[]", "SERVER_PUBKEY_ALT", "new byte[] {(byte) 0x5a, (byte) 0x98, (byte) 0xf2, (byte) 0x3d, (byte) 0xe6, (byte) 0x56, (byte) 0x05, (byte) 0xd0, (byte) 0x50, (byte) 0xdc, (byte) 0x00, (byte) 0x64, (byte) 0xbe, (byte) 0x07, (byte) 0xdd, (byte) 0xdd, (byte) 0x81, (byte) 0x1d, (byte) 0xa1, (byte) 0x16, (byte) 0xa5, (byte) 0x43, (byte) 0xce, (byte) 0x43, (byte) 0xaa, (byte) 0x26, (byte) 0x87, (byte) 0xd1, (byte) 0x9f, (byte) 0x20, (byte) 0xaf, (byte) 0x3c }"
 
 
+            buildConfigField "String", "DIRECTORY_SERVER_URL", "\"https://apip.test.threema.ch/\""
+            buildConfigField "String", "DIRECTORY_SERVER_IPV6_URL", "\"https://ds-apip.test.threema.ch/\""
+            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/\""
+
             // config fields for action URLs / deep links
             // config fields for action URLs / deep links
             buildConfigField "String", "uriScheme", "\"threemawork\""
             buildConfigField "String", "uriScheme", "\"threemawork\""
             buildConfigField "String", "actionUrl", "\"work.threema.ch\""
             buildConfigField "String", "actionUrl", "\"work.threema.ch\""
-            buildConfigField "String", "contactActionUrl", "\"threema.id\""
 
 
             manifestPlaceholders = [
             manifestPlaceholders = [
-                uriScheme: "threemawork",
-                actionUrl: "work.threema.ch",
-                contactActionUrl: "threema.id"
+                uriScheme       : "threemawork",
+                actionUrl       : "work.threema.ch",
             ]
             ]
         }
         }
+        onprem {
+            versionName "${app_version}o${beta_suffix}"
+            applicationId "ch.threema.app.onprem"
+            testApplicationId 'ch.threema.app.onprem.test'
+            resValue "string", "app_name", "Threema OnPrem"
+
+            resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.onprem.profile"
+            resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.onprem.call"
+            buildConfigField "int", "MAX_GROUP_SIZE", "256"
+            buildConfigField "String", "CHAT_SERVER_PREFIX", "\"\""
+            buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"\""
+            buildConfigField "String", "CHAT_SERVER_SUFFIX", "null"
+            buildConfigField "String", "MEDIA_PATH", "\"ThreemaOnPrem\""
+            buildConfigField "boolean", "CHAT_SERVER_GROUPS", "false"
+
+            buildConfigField "byte[]", "SERVER_PUBKEY", "null"
+            buildConfigField "byte[]", "SERVER_PUBKEY_ALT", "null"
+            buildConfigField "String", "DIRECTORY_SERVER_URL", "null"
+            buildConfigField "String", "DIRECTORY_SERVER_IPV6_URL", "null"
+            buildConfigField "String", "BLOB_SERVER_DOWNLOAD_URL", "null"
+            buildConfigField "String", "BLOB_SERVER_DOWNLOAD_IPV6_URL", "null"
+            buildConfigField "String", "BLOB_SERVER_DONE_URL", "null"
+            buildConfigField "String", "BLOB_SERVER_DONE_IPV6_URL", "null"
+            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=\"}"
+
+            // config fields for action URLs / deep links
+            buildConfigField "String", "uriScheme", "\"threemaonprem\""
+            buildConfigField "String", "actionUrl", "\"onprem.threema.ch\""
+
+            manifestPlaceholders = [
+                uriScheme: "threemaonprem",
+                actionUrl: "onprem.threema.ch",
+            ]
+        }
+
         red { // Essentially like sandbox work, but with a different icon and accent color, used for internal testing
         red { // Essentially like sandbox work, but with a different icon and accent color, used for internal testing
             versionName "${app_version}r${beta_suffix}"
             versionName "${app_version}r${beta_suffix}"
             applicationId "ch.threema.app.red"
             applicationId "ch.threema.app.red"
             testApplicationId 'ch.threema.app.red.test'
             testApplicationId 'ch.threema.app.red.test'
-
-            resValue "string", "package_name", applicationId
             resValue "string", "app_name", "Threema Red"
             resValue "string", "app_name", "Threema Red"
+
             resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.redwork.profile"
             resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.redwork.profile"
             resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.redwork.call"
             resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.redwork.call"
-            resValue "integer", "max_group_size", "256"
-            resValue "string", "shop_download_filename", ""
 
 
             buildConfigField "String", "CHAT_SERVER_PREFIX", "\"w-\""
             buildConfigField "String", "CHAT_SERVER_PREFIX", "\"w-\""
-            buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"ds.\""
-            buildConfigField "String", "MEDIA_PATH", "\"ThreemaRed\""
-            buildConfigField "boolean", "CHAT_SERVER_GROUPS", "true"
-
+            buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"ds.w-\""
             buildConfigField "String", "CHAT_SERVER_SUFFIX", "\".0.test.threema.ch\""
             buildConfigField "String", "CHAT_SERVER_SUFFIX", "\".0.test.threema.ch\""
+            buildConfigField "String", "MEDIA_PATH", "\"ThreemaRed\""
             buildConfigField "byte[]", "SERVER_PUBKEY", "new byte[] {(byte) 0x5a, (byte) 0x98, (byte) 0xf2, (byte) 0x3d, (byte) 0xe6, (byte) 0x56, (byte) 0x05, (byte) 0xd0, (byte) 0x50, (byte) 0xdc, (byte) 0x00, (byte) 0x64, (byte) 0xbe, (byte) 0x07, (byte) 0xdd, (byte) 0xdd, (byte) 0x81, (byte) 0x1d, (byte) 0xa1, (byte) 0x16, (byte) 0xa5, (byte) 0x43, (byte) 0xce, (byte) 0x43, (byte) 0xaa, (byte) 0x26, (byte) 0x87, (byte) 0xd1, (byte) 0x9f, (byte) 0x20, (byte) 0xaf, (byte) 0x3c }"
             buildConfigField "byte[]", "SERVER_PUBKEY", "new byte[] {(byte) 0x5a, (byte) 0x98, (byte) 0xf2, (byte) 0x3d, (byte) 0xe6, (byte) 0x56, (byte) 0x05, (byte) 0xd0, (byte) 0x50, (byte) 0xdc, (byte) 0x00, (byte) 0x64, (byte) 0xbe, (byte) 0x07, (byte) 0xdd, (byte) 0xdd, (byte) 0x81, (byte) 0x1d, (byte) 0xa1, (byte) 0x16, (byte) 0xa5, (byte) 0x43, (byte) 0xce, (byte) 0x43, (byte) 0xaa, (byte) 0x26, (byte) 0x87, (byte) 0xd1, (byte) 0x9f, (byte) 0x20, (byte) 0xaf, (byte) 0x3c }"
             buildConfigField "byte[]", "SERVER_PUBKEY_ALT", "new byte[] {(byte) 0x5a, (byte) 0x98, (byte) 0xf2, (byte) 0x3d, (byte) 0xe6, (byte) 0x56, (byte) 0x05, (byte) 0xd0, (byte) 0x50, (byte) 0xdc, (byte) 0x00, (byte) 0x64, (byte) 0xbe, (byte) 0x07, (byte) 0xdd, (byte) 0xdd, (byte) 0x81, (byte) 0x1d, (byte) 0xa1, (byte) 0x16, (byte) 0xa5, (byte) 0x43, (byte) 0xce, (byte) 0x43, (byte) 0xaa, (byte) 0x26, (byte) 0x87, (byte) 0xd1, (byte) 0x9f, (byte) 0x20, (byte) 0xaf, (byte) 0x3c }"
             buildConfigField "byte[]", "SERVER_PUBKEY_ALT", "new byte[] {(byte) 0x5a, (byte) 0x98, (byte) 0xf2, (byte) 0x3d, (byte) 0xe6, (byte) 0x56, (byte) 0x05, (byte) 0xd0, (byte) 0x50, (byte) 0xdc, (byte) 0x00, (byte) 0x64, (byte) 0xbe, (byte) 0x07, (byte) 0xdd, (byte) 0xdd, (byte) 0x81, (byte) 0x1d, (byte) 0xa1, (byte) 0x16, (byte) 0xa5, (byte) 0x43, (byte) 0xce, (byte) 0x43, (byte) 0xaa, (byte) 0x26, (byte) 0x87, (byte) 0xd1, (byte) 0x9f, (byte) 0x20, (byte) 0xaf, (byte) 0x3c }"
+            buildConfigField "String", "DIRECTORY_SERVER_URL", "\"https://apip.test.threema.ch/\""
+            buildConfigField "String", "DIRECTORY_SERVER_IPV6_URL", "\"https://ds-apip.test.threema.ch/\""
+            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 "boolean", "SEND_CONSUMED_DELIVERY_RECEIPTS", "true"
             buildConfigField "boolean", "SEND_CONSUMED_DELIVERY_RECEIPTS", "true"
 
 
             // config fields for action URLs / deep links
             // config fields for action URLs / deep links
-            buildConfigField "String", "uriScheme", "\"threemawork\""
-            buildConfigField "String", "actionUrl", "\"work.threema.ch\""
-            buildConfigField "String", "contactActionUrl", "\"threema.id\""
+            buildConfigField "String", "uriScheme", "\"threemared\""
+            buildConfigField "String", "actionUrl", "\"red.threema.ch\""
 
 
             manifestPlaceholders = [
             manifestPlaceholders = [
-                uriScheme: "threemawork",
-                actionUrl: "work.threema.ch",
-                contactActionUrl: "threema.id"
+                uriScheme: "threemared",
+                actionUrl: "red.threema.ch",
             ]
             ]
         }
         }
         hms {
         hms {
             applicationId "ch.threema.app.hms"
             applicationId "ch.threema.app.hms"
-            resValue "string", "package_name", applicationId
         }
         }
         hms_work {
         hms_work {
             versionName "${app_version}k${beta_suffix}"
             versionName "${app_version}k${beta_suffix}"
             applicationId "ch.threema.app.work.hms"
             applicationId "ch.threema.app.work.hms"
             testApplicationId 'ch.threema.app.work.test.hms'
             testApplicationId 'ch.threema.app.work.test.hms'
-            resValue "string", "package_name", applicationId
             resValue "string", "app_name", "Threema Work"
             resValue "string", "app_name", "Threema Work"
+
             resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.work.profile"
             resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.work.profile"
             resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.work.call"
             resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.work.call"
-            resValue "integer", "max_group_size", "256"
-            resValue "string", "shop_download_filename", ""
             buildConfigField "String", "CHAT_SERVER_PREFIX", "\"w-\""
             buildConfigField "String", "CHAT_SERVER_PREFIX", "\"w-\""
-            buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"ds.\""
-            buildConfigField "String", "CHAT_SERVER_SUFFIX", "\".0.threema.ch\""
+            buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"ds.w-\""
             buildConfigField "String", "MEDIA_PATH", "\"ThreemaWork\""
             buildConfigField "String", "MEDIA_PATH", "\"ThreemaWork\""
-            buildConfigField "boolean", "CHAT_SERVER_GROUPS", "true"
+            buildConfigField "String", "WORK_SERVER_URL", "\"https://apip-work.threema.ch/\""
+            buildConfigField "String", "WORK_SERVER_IPV6_URL", "\"https://ds-apip-work.threema.ch/\""
 
 
             // config fields for action URLs / deep links
             // config fields for action URLs / deep links
             buildConfigField "String", "uriScheme", "\"threemawork\""
             buildConfigField "String", "uriScheme", "\"threemawork\""
             buildConfigField "String", "actionUrl", "\"work.threema.ch\""
             buildConfigField "String", "actionUrl", "\"work.threema.ch\""
-            buildConfigField "String", "contactActionUrl", "\"threema.id\""
 
 
             manifestPlaceholders = [
             manifestPlaceholders = [
                 uriScheme: "threemawork",
                 uriScheme: "threemawork",
                 actionUrl: "work.threema.ch",
                 actionUrl: "work.threema.ch",
-                contactActionUrl: "threema.id"
             ]
             ]
         }
         }
     }
     }
@@ -326,17 +380,40 @@ android {
         } else {
         } else {
             logger.warn("No hms keystore found. Falling back to locally generated keystore.")
             logger.warn("No hms keystore found. Falling back to locally generated keystore.")
         }
         }
+
+        // Onprem release config
+        if (keystores.onprem_release != null) {
+            onprem_release {
+                storeFile file(keystores.onprem_release.storeFile)
+                storePassword keystores.onprem_release.storePassword
+                keyAlias keystores.onprem_release.keyAlias
+                keyPassword keystores.onprem_release.keyPassword
+            }
+        } else {
+            logger.warn("No onprem keystore found. Falling back to locally generated keystore.")
+        }
+
+        // Red release config
+        if (keystores.red_release != null) {
+            red_release {
+                storeFile file(keystores.red_release.storeFile)
+                storePassword keystores.red_release.storePassword
+                keyAlias keystores.red_release.keyAlias
+                keyPassword keystores.red_release.keyPassword
+            }
+        } else {
+            logger.warn("No red keystore found. Falling back to locally generated keystore.")
+        }
     }
     }
 
 
     sourceSets {
     sourceSets {
-        none {
-            java.srcDir 'src/google_services_based/java'
-            manifest.srcFile 'src/store_google/AndroidManifest.xml'
-        }
         main {
         main {
             assets.srcDirs = ['assets']
             assets.srcDirs = ['assets']
             jniLibs.srcDirs = ['libs']
             jniLibs.srcDirs = ['libs']
         }
         }
+        none {
+            java.srcDir 'src/google_services_based/java'
+        }
         store_google {
         store_google {
             java.srcDir 'src/google_services_based/java'
             java.srcDir 'src/google_services_based/java'
         }
         }
@@ -353,6 +430,9 @@ android {
             java.srcDir 'src/hms_services_based/java'
             java.srcDir 'src/hms_services_based/java'
             res.srcDir 'src/store_google_work/res'
             res.srcDir 'src/store_google_work/res'
         }
         }
+        onprem {
+            java.srcDir 'src/google_services_based/java'
+        }
         sandbox {
         sandbox {
             java.srcDir 'src/google_services_based/java'
             java.srcDir 'src/google_services_based/java'
             manifest.srcFile 'src/store_google/AndroidManifest.xml'
             manifest.srcFile 'src/store_google/AndroidManifest.xml'
@@ -374,6 +454,7 @@ android {
             jniDebuggable false
             jniDebuggable false
             multiDexEnabled true
             multiDexEnabled true
             multiDexKeepProguard file('multidex-keep.pro')
             multiDexKeepProguard file('multidex-keep.pro')
+            testCoverageEnabled false
 
 
             if (keystores['debug'] != null) {
             if (keystores['debug'] != null) {
                 signingConfig signingConfigs.debug
                 signingConfig signingConfigs.debug
@@ -393,7 +474,6 @@ android {
                 productFlavors.store_google.signingConfig signingConfigs.release
                 productFlavors.store_google.signingConfig signingConfigs.release
                 productFlavors.store_google_work.signingConfig signingConfigs.release
                 productFlavors.store_google_work.signingConfig signingConfigs.release
                 productFlavors.store_threema.signingConfig signingConfigs.release
                 productFlavors.store_threema.signingConfig signingConfigs.release
-                productFlavors.red.signingConfig signingConfigs.release
                 productFlavors.sandbox.signingConfig signingConfigs.release
                 productFlavors.sandbox.signingConfig signingConfigs.release
                 productFlavors.sandbox_work.signingConfig signingConfigs.release
                 productFlavors.sandbox_work.signingConfig signingConfigs.release
                 productFlavors.none.signingConfig signingConfigs.release
                 productFlavors.none.signingConfig signingConfigs.release
@@ -403,9 +483,31 @@ android {
                 productFlavors.hms.signingConfig signingConfigs.hms_release
                 productFlavors.hms.signingConfig signingConfigs.hms_release
                 productFlavors.hms_work.signingConfig signingConfigs.hms_release
                 productFlavors.hms_work.signingConfig signingConfigs.hms_release
             }
             }
+
+            if (keystores['onprem_release'] != null) {
+                productFlavors.onprem.signingConfig signingConfigs.onprem_release
+            }
+
+            if (keystores['red_release'] != null) {
+                productFlavors.red.signingConfig signingConfigs.red_release
+            }
+        }
+    }
+
+    // Only build relevant buildType / flavor combinations
+    variantFilter { variant ->
+        def names = variant.flavors*.name
+
+        if (
+            variant.buildType.name == "release" && (
+                names.contains("sandbox") || names.contains("sandbox_work")
+            )
+        ) {
+            setIgnore(true)
         }
         }
     }
     }
 
 
+
     externalNativeBuild {
     externalNativeBuild {
         ndkBuild {
         ndkBuild {
             path 'jni/Android.mk'
             path 'jni/Android.mk'
@@ -465,6 +567,7 @@ android {
         exclude 'META-INF/license.txt'
         exclude 'META-INF/license.txt'
         exclude 'META-INF/dependencies.txt'
         exclude 'META-INF/dependencies.txt'
         exclude 'META-INF/LGPL2.1'
         exclude 'META-INF/LGPL2.1'
+        exclude '**/*.proto'
         // fix https://stackoverflow.com/questions/42739916/aarch64-linux-android-strip-file-missing
         // fix https://stackoverflow.com/questions/42739916/aarch64-linux-android-strip-file-missing
         doNotStrip '*/mips/*.so'
         doNotStrip '*/mips/*.so'
         doNotStrip '*/mips64/*.so'
         doNotStrip '*/mips64/*.so'
@@ -512,6 +615,8 @@ dependencies {
         //resolutionStrategy.failOnVersionConflict()
         //resolutionStrategy.failOnVersionConflict()
     }
     }
 
 
+    implementation project(':domain')
+
     implementation 'net.zetetic:android-database-sqlcipher:4.4.3'
     implementation 'net.zetetic:android-database-sqlcipher:4.4.3'
 
 
     implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
     implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
@@ -521,7 +626,7 @@ dependencies {
     implementation 'com.mapbox.mapboxsdk:mapbox-android-sdk:9.2.1'
     implementation 'com.mapbox.mapboxsdk:mapbox-android-sdk:9.2.1'
     // commons-io >2.6 requires android 8
     // commons-io >2.6 requires android 8
     implementation 'commons-io:commons-io:2.6'
     implementation 'commons-io:commons-io:2.6'
-    implementation 'org.slf4j:slf4j-api:1.7.30'
+    implementation "org.slf4j:slf4j-api:$slf4j_version"
     implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.23'
     implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.23'
     implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
     implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
     implementation 'com.datatheorem.android.trustkit:trustkit:1.1.5'
     implementation 'com.datatheorem.android.trustkit:trustkit:1.1.5'
@@ -535,11 +640,11 @@ dependencies {
     implementation 'androidx.palette:palette:1.0.0'
     implementation 'androidx.palette:palette:1.0.0'
     implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
     implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
     implementation 'androidx.appcompat:appcompat:1.3.1'
     implementation 'androidx.appcompat:appcompat:1.3.1'
-    implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
+    implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
     implementation 'androidx.biometric:biometric:1.1.0'
     implementation 'androidx.biometric:biometric:1.1.0'
-    implementation "androidx.work:work-runtime:2.5.0"
+    implementation "androidx.work:work-runtime:2.6.0"
     implementation 'androidx.fragment:fragment:1.3.6'
     implementation 'androidx.fragment:fragment:1.3.6'
-    implementation 'androidx.activity:activity:1.2.4'
+    implementation 'androidx.activity:activity:1.3.1'
     implementation 'androidx.sqlite:sqlite:2.1.0'
     implementation 'androidx.sqlite:sqlite:2.1.0'
     implementation "androidx.concurrent:concurrent-futures:1.1.0"
     implementation "androidx.concurrent:concurrent-futures:1.1.0"
     implementation "androidx.camera:camera-camera2:1.0.2"
     implementation "androidx.camera:camera-camera2:1.0.2"
@@ -548,6 +653,7 @@ dependencies {
     implementation "androidx.camera:camera-view:1.0.0-alpha25"
     implementation "androidx.camera:camera-view:1.0.0-alpha25"
     implementation 'androidx.multidex:multidex:2.0.1'
     implementation 'androidx.multidex:multidex:2.0.1'
     implementation "androidx.lifecycle:lifecycle-viewmodel:2.3.1"
     implementation "androidx.lifecycle:lifecycle-viewmodel:2.3.1"
+    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
     implementation "androidx.lifecycle:lifecycle-livedata:2.3.1"
     implementation "androidx.lifecycle:lifecycle-livedata:2.3.1"
     implementation "androidx.lifecycle:lifecycle-runtime:2.3.1"
     implementation "androidx.lifecycle:lifecycle-runtime:2.3.1"
     implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1"
     implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1"
@@ -558,11 +664,10 @@ dependencies {
     implementation "androidx.paging:paging-runtime:3.0.1"
     implementation "androidx.paging:paging-runtime:3.0.1"
 
 
     implementation 'com.google.android.material:material:1.4.0'
     implementation 'com.google.android.material:material:1.4.0'
-    implementation 'com.google.android.exoplayer:exoplayer-core:2.13.3'
-    implementation 'com.google.android.exoplayer:exoplayer-ui:2.13.3'
-    implementation 'com.google.protobuf:protobuf-javalite:3.9.1'
+    implementation 'com.google.android.exoplayer:exoplayer-core:2.15.1'
+    implementation 'com.google.android.exoplayer:exoplayer-ui:2.15.1'
     implementation 'com.google.zxing:core:3.3.3' // zxing 3.4 crashes on kitkat
     implementation 'com.google.zxing:core:3.3.3' // zxing 3.4 crashes on kitkat
-    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.26'
+    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.37'
 
 
     // webclient dependencies
     // webclient dependencies
     implementation 'org.msgpack:msgpack-core:0.8.22!!'
     implementation 'org.msgpack:msgpack-core:0.8.22!!'
@@ -572,14 +677,14 @@ dependencies {
     implementation 'net.sourceforge.streamsupport:streamsupport-cfuture:1.7.2'
     implementation 'net.sourceforge.streamsupport:streamsupport-cfuture:1.7.2'
 
 
     // Google Assistant Voice Action verification library
     // Google Assistant Voice Action verification library
-    implementation(name:'libgsaverification-client', ext:'aar')
+    implementation(name: 'libgsaverification-client', ext: 'aar')
 
 
     implementation('org.saltyrtc:saltyrtc-client:0.14.2') {
     implementation('org.saltyrtc:saltyrtc-client:0.14.2') {
         exclude group: 'org.json'
         exclude group: 'org.json'
     }
     }
 
 
     implementation 'org.saltyrtc:chunked-dc:1.0.1'
     implementation 'org.saltyrtc:chunked-dc:1.0.1'
-    implementation 'ch.threema:webrtc-android:91.0.1'
+    implementation 'ch.threema:webrtc-android:94.0.0'
     implementation('org.saltyrtc:saltyrtc-task-webrtc:0.18.1') {
     implementation('org.saltyrtc:saltyrtc-task-webrtc:0.18.1') {
         exclude module: 'saltyrtc-client'
         exclude module: 'saltyrtc-client'
     }
     }
@@ -588,11 +693,18 @@ dependencies {
     implementation 'com.github.bumptech.glide:glide:4.12.0'
     implementation 'com.github.bumptech.glide:glide:4.12.0'
     annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
     annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
 
 
+    // kotlin
+    implementation "androidx.core:core-ktx:1.3.2"
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
+
+
     // use leak canary in debug builds
     // use leak canary in debug builds
 //    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.5'
 //    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.5'
 
 
     // test dependencies
     // test dependencies
     testImplementation 'junit:junit:4.12'
     testImplementation 'junit:junit:4.12'
+    testImplementation(testFixtures(project(":domain")))
 
 
     // use powermock instead of mockito. it support mocking static classes.
     // use powermock instead of mockito. it support mocking static classes.
     def mockitoVersion = '2.0.7'
     def mockitoVersion = '2.0.7'
@@ -604,6 +716,9 @@ dependencies {
     // add JSON support to tests without mocking
     // add JSON support to tests without mocking
     testImplementation 'org.json:json:20160212'
     testImplementation 'org.json:json:20160212'
 
 
+    testImplementation 'com.tngtech.archunit:archunit-junit4:0.18.0'
+
+    androidTestImplementation(testFixtures(project(":domain")))
     androidTestImplementation 'androidx.test:rules:1.2.0'
     androidTestImplementation 'androidx.test:rules:1.2.0'
     androidTestImplementation 'tools.fastlane:screengrab:2.0.0', {
     androidTestImplementation 'tools.fastlane:screengrab:2.0.0', {
         exclude group: 'androidx.annotation', module: 'annotation'
         exclude group: 'androidx.annotation', module: 'annotation'
@@ -652,6 +767,7 @@ dependencies {
         store_googleImplementation(dependency) { excludes.each { exclude it } }
         store_googleImplementation(dependency) { excludes.each { exclude it } }
         store_google_workImplementation(dependency) { excludes.each { exclude it } }
         store_google_workImplementation(dependency) { excludes.each { exclude it } }
         store_threemaImplementation(dependency) { excludes.each { exclude it } }
         store_threemaImplementation(dependency) { excludes.each { exclude it } }
+        onpremImplementation(dependency) { excludes.each { exclude it } }
         sandboxImplementation(dependency) { excludes.each { exclude it } }
         sandboxImplementation(dependency) { excludes.each { exclude it } }
         sandbox_workImplementation(dependency) { excludes.each { exclude it } }
         sandbox_workImplementation(dependency) { excludes.each { exclude it } }
         redImplementation(dependency) { excludes.each { exclude it } }
         redImplementation(dependency) { excludes.each { exclude it } }
@@ -677,16 +793,13 @@ dependencies {
 
 
 sonarqube {
 sonarqube {
     properties {
     properties {
-        property "sonar.projectKey", "android-client"
-        property "sonar.projectName", "Threema for Android"
         property "sonar.sources", "src/main/, ../scripts/, ../scripts-internal/"
         property "sonar.sources", "src/main/, ../scripts/, ../scripts-internal/"
-        // Exclusion notes:
-        // - Protobuf code is generated
-        // - Java Client code (including jnacl) is already being checked by SonarQube separately
-        property "sonar.exclusions", "src/main/java/ch/threema/protobuf/**, src/main/java/ch/threema/client/**, src/test/java/ch/threema/client/**, src/main/java/ch/threema/base/**, src/main/java/ch/threema/localcrypto/**, src/test/java/ch/threema/localcrypto/**, src/main/java/com/neilalexander/jnacl/**"
-        property "sonar.tests", "src/test/"
+        property "sonar.exclusions", "src/main/java/ch/threema/localcrypto/**, src/test/java/ch/threema/localcrypto/**"
+        property "sonar.tests", "src/testNoneDebug/"
         property "sonar.sourceEncoding", "UTF-8"
         property "sonar.sourceEncoding", "UTF-8"
         property "sonar.verbose", "true"
         property "sonar.verbose", "true"
+        property 'sonar.projectKey', 'android-client'
+        property 'sonar.projectName', 'Threema for Android'
     }
     }
 }
 }
 
 

+ 1 - 1
app/proguard-project.txt

@@ -252,4 +252,4 @@ public static <fields>;
 # Adding this line fixes the problem. It's most probably a bug in the R8 optimizer
 # Adding this line fixes the problem. It's most probably a bug in the R8 optimizer
 # that's triggered by this file, the NonceFactory wasn't modified since 2017...
 # that's triggered by this file, the NonceFactory wasn't modified since 2017...
 # Maybe we can remove this again with a future build toolchain.
 # Maybe we can remove this again with a future build toolchain.
--keep class ch.threema.client.NonceFactory { *; }
+-keep class ch.threema.base.crypto.NonceFactory { *; }

+ 10 - 4
app/src/androidTest/java/ch/threema/app/backuprestore/csv/BackupServiceTest.java

@@ -24,6 +24,7 @@ package ch.threema.app.backuprestore.csv;
 import android.Manifest;
 import android.Manifest;
 import android.content.Context;
 import android.content.Context;
 import android.content.Intent;
 import android.content.Intent;
+import android.os.Build;
 import android.util.Log;
 import android.util.Log;
 
 
 import net.lingala.zip4j.ZipFile;
 import net.lingala.zip4j.ZipFile;
@@ -35,6 +36,7 @@ import org.apache.commons.io.IOUtils;
 import org.junit.Assert;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.BeforeClass;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runner.RunWith;
@@ -47,13 +49,12 @@ import java.util.List;
 import java.util.Objects;
 import java.util.Objects;
 
 
 import androidx.annotation.NonNull;
 import androidx.annotation.NonNull;
-import androidx.core.content.ContextCompat;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.LargeTest;
 import androidx.test.rule.GrantPermissionRule;
 import androidx.test.rule.GrantPermissionRule;
 import ch.threema.app.DangerousTest;
 import ch.threema.app.DangerousTest;
-import ch.threema.app.TestHelpers;
+import ch.threema.app.testutils.TestHelpers;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.backuprestore.BackupRestoreDataConfig;
 import ch.threema.app.backuprestore.BackupRestoreDataConfig;
 import ch.threema.app.exceptions.FileSystemNotPresentException;
 import ch.threema.app.exceptions.FileSystemNotPresentException;
@@ -67,7 +68,7 @@ import ch.threema.app.services.MessageService;
 import ch.threema.app.services.ballot.BallotService;
 import ch.threema.app.services.ballot.BallotService;
 import ch.threema.app.utils.CSVReader;
 import ch.threema.app.utils.CSVReader;
 import ch.threema.app.utils.CSVRow;
 import ch.threema.app.utils.CSVRow;
-import ch.threema.client.IdentityBackupDecoder;
+import ch.threema.domain.identitybackup.IdentityBackupDecoder;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.data.status.VoipStatusDataModel;
 import ch.threema.storage.models.data.status.VoipStatusDataModel;
 import java8.util.stream.StreamSupport;
 import java8.util.stream.StreamSupport;
@@ -75,6 +76,7 @@ import java8.util.stream.StreamSupport;
 @RunWith(AndroidJUnit4.class)
 @RunWith(AndroidJUnit4.class)
 @LargeTest
 @LargeTest
 @DangerousTest // Deletes data and possibly identity
 @DangerousTest // Deletes data and possibly identity
+@Ignore("because this test broke with API version switch introduced in 7ed52bcfedd0bdcd2924ae14afe7ccb7bdc52c7a") // TODO(ANDR-1483)
 public class BackupServiceTest {
 public class BackupServiceTest {
 	private final static String PASSWORD = "ubnpwrgujioasdfi0932";
 	private final static String PASSWORD = "ubnpwrgujioasdfi0932";
 	private static final String TAG = "BackupServiceTest";
 	private static final String TAG = "BackupServiceTest";
@@ -148,7 +150,11 @@ public class BackupServiceTest {
 		intent.putExtra(BackupService.EXTRA_BACKUP_RESTORE_DATA_CONFIG, config);
 		intent.putExtra(BackupService.EXTRA_BACKUP_RESTORE_DATA_CONFIG, config);
 
 
 		// Start service
 		// Start service
-		ContextCompat.startForegroundService(appContext, intent);
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+			appContext.startForegroundService(intent);
+		}
+
+		appContext.startService(intent);
 		Assert.assertTrue(TestHelpers.iServiceRunning(appContext, BackupService.class));
 		Assert.assertTrue(TestHelpers.iServiceRunning(appContext, BackupService.class));
 
 
 		// Wait for service to stop
 		// Wait for service to stop

+ 74 - 0
app/src/androidTest/java/ch/threema/app/processors/MessageAckProcessorTest.java

@@ -0,0 +1,74 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2021 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.app.processors;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Objects;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import ch.threema.app.ThreemaApplication;
+import ch.threema.app.managers.ServiceManager;
+import ch.threema.app.services.MessageService;
+import ch.threema.app.testutils.CaptureLogcatOnTestFailureRule;
+import ch.threema.domain.models.MessageId;
+import ch.threema.domain.models.QueueMessageId;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class MessageAckProcessorTest {
+	// Test rules
+	@Rule public CaptureLogcatOnTestFailureRule captureLogcatOnTestFailureRule = new CaptureLogcatOnTestFailureRule();
+
+	// Services
+	private MessageService messageService;
+
+	// Message ack processor
+	private MessageAckProcessor messageAckProcessor;
+
+	@Before
+	public void setUp() throws Exception {
+		// Load services
+		final ServiceManager serviceManager = Objects.requireNonNull(ThreemaApplication.getServiceManager());
+		this.messageService = serviceManager.getMessageService();
+
+		// Create processor
+		this.messageAckProcessor = new MessageAckProcessor();
+		this.messageAckProcessor.setMessageService(this.messageService);
+	}
+
+	/**
+	 * Ensure that {@link MessageAckProcessor#wasRecentlyAcked(MessageId)} works.
+	 */
+	@Test
+	public void wasRecentlyAcked() {
+		final QueueMessageId queueMessageId = new QueueMessageId(new MessageId(), "09BNNVR2");
+		Assert.assertFalse(this.messageAckProcessor.wasRecentlyAcked(queueMessageId.getMessageId()));
+		this.messageAckProcessor.processAck(queueMessageId);
+		Assert.assertTrue(this.messageAckProcessor.wasRecentlyAcked(queueMessageId.getMessageId()));
+	}
+}

+ 244 - 0
app/src/androidTest/java/ch/threema/app/processors/MessageProcessorTest.java

@@ -0,0 +1,244 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2021 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.app.processors;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+
+import java.util.Objects;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import ch.threema.app.ThreemaApplication;
+import ch.threema.app.managers.ServiceManager;
+import ch.threema.app.services.ContactService;
+import ch.threema.app.services.FileService;
+import ch.threema.app.services.GroupService;
+import ch.threema.app.services.IdListService;
+import ch.threema.app.services.MessageService;
+import ch.threema.app.services.NotificationService;
+import ch.threema.app.services.PreferenceService;
+import ch.threema.app.services.ballot.BallotService;
+import ch.threema.app.services.group.GroupInviteService;
+import ch.threema.app.services.group.GroupJoinResponseService;
+import ch.threema.app.services.group.IncomingGroupJoinRequestService;
+import ch.threema.app.testutils.CaptureLogcatOnTestFailureRule;
+import ch.threema.app.testutils.TestHelpers;
+import ch.threema.app.testutils.ThreemaAssert;
+import ch.threema.app.voip.services.VoipStateService;
+import ch.threema.base.ThreemaException;
+import ch.threema.base.crypto.NonceFactory;
+import ch.threema.base.utils.Utils;
+import ch.threema.domain.helpers.InMemoryContactStore;
+import ch.threema.domain.helpers.InMemoryIdentityStore;
+import ch.threema.domain.helpers.InMemoryNonceStore;
+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;
+import ch.threema.domain.protocol.csp.coders.MessageCoder;
+import ch.threema.domain.protocol.csp.connection.MessageProcessorInterface.ProcessIncomingResult;
+import ch.threema.domain.protocol.csp.messages.DeliveryReceiptMessage;
+import ch.threema.domain.stores.ContactStore;
+import ch.threema.domain.stores.IdentityStoreInterface;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class MessageProcessorTest {
+	// Test rules
+	@Rule public TestName name = new TestName();
+	@Rule public CaptureLogcatOnTestFailureRule captureLogcatOnTestFailureRule = new CaptureLogcatOnTestFailureRule();
+
+	private final static Contact TEST_CONTACT_1 = new Contact("09BNNVR2", Utils.hexStringToByteArray("e4613bbe5408d342fdabc3edf4509d1a3aecd7cb0598773987eef8400e74c81a"));
+	private final static Contact TEST_CONTACT_2 = new Contact("0BSXZ4P8", Utils.hexStringToByteArray("dee1cd341de88f783a768941eac702951c8bbb21e836da4a43ab8f3776fc0a65"));
+
+	// Services
+	private MessageService messageService;
+	private ContactService contactService;
+	private PreferenceService preferenceService;
+	private GroupService groupService;
+	private GroupInviteService groupInviteService;
+	private GroupJoinResponseService groupJoinResponseService;
+	private IncomingGroupJoinRequestService incomingGroupJoinRequestService;
+	private IdListService blackListService;
+	private BallotService ballotService;
+	private FileService fileService;
+	private NotificationService notificationService;
+	private VoipStateService voipStateService;
+	private NonceFactory nonceFactory;
+
+	// Stores
+	private IdentityStoreInterface identityStore;
+	private IdentityStoreInterface identityStore2;
+	private ContactStore contactStore;
+
+	// Message processor
+	private MessageProcessor messageProcessor;
+
+	@Before
+	public void setUp() throws Exception {
+		// Load services
+		final ServiceManager serviceManager = Objects.requireNonNull(ThreemaApplication.getServiceManager());
+		this.messageService = serviceManager.getMessageService();
+		this.contactService = serviceManager.getContactService();
+		this.preferenceService = serviceManager.getPreferenceService();
+		this.groupService = serviceManager.getGroupService();
+		this.groupInviteService = serviceManager.getGroupInviteService();
+		this.groupJoinResponseService = serviceManager.getGroupJoinResponseService();
+		this.incomingGroupJoinRequestService = serviceManager.getIncomingGroupJoinRequestService();
+		this.blackListService = serviceManager.getBlackListService();
+		this.ballotService = serviceManager.getBallotService();
+		this.fileService = serviceManager.getFileService();
+		this.notificationService = serviceManager.getNotificationService();
+		this.voipStateService = serviceManager.getVoipStateService();
+		this.nonceFactory = new NonceFactory(new InMemoryNonceStore());
+
+		// Create in-memory stores
+		this.contactStore = new InMemoryContactStore();
+
+		// Create two identities
+		this.identityStore = new InMemoryIdentityStore(
+			"07N3PDDA",
+			"86",
+			Utils.hexStringToByteArray("e2c457e6f90b9e4dd1f9feedb078382d1dbd1e64c616b2e8ac8ae28b8cede36e"),
+			"07N3PDDA");
+		this.identityStore2 = new InMemoryIdentityStore(
+			"07ZKBCYU",
+			"3a",
+			Utils.hexStringToByteArray("4a7983f3e4dc7d5d1a591a94dfc03b16b94a4ca5a15e4e68c3bdba4dd030dd3e"),
+			"07ZKBCYU"
+		);
+
+		// Store contacts (including ourselves, so we can encrypt messages to ourselves)
+		this.contactStore.addContact(TEST_CONTACT_1);
+		this.contactStore.addContact(TEST_CONTACT_2);
+		this.contactStore.addContact(new Contact(this.identityStore.getIdentity(), this.identityStore.getPublicKey()));
+		this.contactStore.addContact(new Contact(this.identityStore2.getIdentity(), this.identityStore2.getPublicKey()));
+
+		// Create message processor
+		this.messageProcessor = new MessageProcessor(
+			this.messageService,
+			this.contactService,
+			this.identityStore,
+			this.contactStore,
+			this.preferenceService,
+			this.groupService,
+			this.groupJoinResponseService,
+			this.incomingGroupJoinRequestService,
+			this.blackListService,
+			this.ballotService,
+			this.fileService,
+			this.notificationService,
+			this.voipStateService
+		);
+	}
+
+	/**
+	 * Return logcat logs for this test (without clearing the log).
+	 */
+	private String getLogs() {
+		final String testName = this.name.getMethodName() + "(" + MessageProcessorTest.class.getName() + ")";
+		return TestHelpers.getTestLogs(testName);
+	}
+
+	/**
+	 * When a message is processed that is not directed at us, processing fails.
+	 */
+	@Test
+	@Ignore("because getLogs does not work consistently.") // TODO(ANDR-1484)
+	public void messageForOtherIdentity() throws ThreemaException {
+		final MessageBox boxmsg = new MessageBox();
+		boxmsg.setFromIdentity(TEST_CONTACT_1.getIdentity());
+		boxmsg.setToIdentity(TEST_CONTACT_2.getIdentity());
+		boxmsg.setBox(new byte[] { 0, 1, 2, 3 });
+		boxmsg.setNonce(this.nonceFactory.next());
+		final ProcessIncomingResult result = this.messageProcessor.processIncomingMessage(boxmsg);
+		Assert.assertFalse(result.processed);
+		final String logs = this.getLogs();
+		ThreemaAssert.assertContains(logs, "BadMessageException: Message is not for own identity, cannot decode");
+	}
+
+	/**
+	 * When processing an invalid box, no exception should be thrown.
+	 */
+	@Test
+	@Ignore("because getLogs does not work consistently.") // TODO(ANDR-1484)
+	public void processInvalidBox() throws ThreemaException {
+		final MessageBox boxmsg = new MessageBox();
+		boxmsg.setFromIdentity(TEST_CONTACT_1.getIdentity());
+		boxmsg.setToIdentity(this.identityStore.getIdentity());
+		boxmsg.setBox(new byte[] { 0, 1, 2, 3 });
+		boxmsg.setNonce(this.nonceFactory.next());
+		final ProcessIncomingResult result = this.messageProcessor.processIncomingMessage(boxmsg);
+		Assert.assertFalse(result.processed);
+		final String logs = this.getLogs();
+		ThreemaAssert.assertContains(logs, "ch.threema.domain.protocol.csp.messages.BadMessageException: Decryption of message from");
+	}
+
+	/**
+	 * Process a delivery receipt for an unknown message.
+	 *
+	 * Because the confirmed message is not known, a log is created,
+	 * but nothing in the database is changed.
+	 */
+	@Test
+	@Ignore("because getLogs does not work consistently.") // TODO(ANDR-1484)
+	public void processDeliveryReceiptForUnknownMessage() throws ThreemaException {
+		// Message IDs
+		final MessageId deliveryMessageId = new MessageId();
+		final MessageId deliveredMessageId = new MessageId();
+
+		// Create message box
+		final DeliveryReceiptMessage msg = new DeliveryReceiptMessage();
+		msg.setMessageId(deliveryMessageId);
+		msg.setFromIdentity(this.identityStore2.getIdentity());
+		msg.setToIdentity(this.identityStore.getIdentity());
+		msg.setReceiptType(ProtocolDefines.DELIVERYRECEIPT_MSGRECEIVED);
+		msg.setReceiptMessageIds(new MessageId[] { deliveredMessageId });
+
+		MessageCoder messageCoder = new MessageCoder(this.contactStore, this.identityStore2);
+		final MessageBox boxmsg = messageCoder.encode(msg, this.nonceFactory);
+
+		// Process message
+		final ProcessIncomingResult result = this.messageProcessor.processIncomingMessage(boxmsg);
+		Assert.assertTrue(result.processed);
+
+		// Assert log messages
+		final String logs = this.getLogs();
+		ThreemaAssert.assertContains(
+			logs,
+			"MessageProcessor: Incoming message " + deliveryMessageId.toString()
+				+ " from " + this.identityStore2.getIdentity()
+				+ " to " + this.identityStore.getIdentity()
+				+ " (type " + Utils.byteToHex((byte) ProtocolDefines.MSGTYPE_DELIVERY_RECEIPT, false, true) + ")"
+		);
+		ThreemaAssert.assertContains(
+			logs,
+			"MessageServiceImpl: Updated message state (DELIVERED) for unknown message with id " + deliveredMessageId.toString()
+		);
+	}
+}

+ 308 - 0
app/src/androidTest/java/ch/threema/app/service/GroupInviteServiceTest.java

@@ -0,0 +1,308 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2021 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.app.service;
+
+import android.accounts.Account;
+import android.accounts.AccountManagerCallback;
+import android.net.Uri;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Date;
+
+import androidx.annotation.Nullable;
+import ch.threema.app.BuildConfig;
+import ch.threema.app.ThreemaApplication;
+import ch.threema.app.exceptions.FileSystemNotPresentException;
+import ch.threema.app.services.GroupService;
+import ch.threema.app.services.UserService;
+import ch.threema.app.services.group.GroupInviteService;
+import ch.threema.app.services.group.GroupInviteServiceImpl;
+import ch.threema.app.services.license.LicenseService;
+import ch.threema.domain.protocol.csp.messages.group.GroupInviteData;
+import ch.threema.domain.protocol.csp.messages.group.GroupInviteToken;
+import ch.threema.localcrypto.MasterKeyLockedException;
+import ch.threema.protobuf.url_payloads.GroupInvite;
+import ch.threema.storage.DatabaseServiceNew;
+import ch.threema.storage.models.group.GroupInviteModel;
+
+public class GroupInviteServiceTest {
+
+	private GroupService groupService;
+	private GroupInviteService groupInviteService;
+
+	static final String TEST_GROUP_NAME = "A nice little group";
+	static final String TEST_INVITE_NAME = "New unnamed link";
+	static String TEST_IDENTITY = "ECHOECHO";
+	static final GroupInvite.InviteType TEST_INVITE_TYPE_AUTOMATIC = GroupInvite.InviteType.AUTOMATIC;
+	static GroupInviteToken TEST_TOKEN_VALID;
+	static GroupInviteModel TEST_INVITE_MODEL;
+	static String TEST_ENCODED_INVITE = "RUNIT0VDSE86MDAwMTAyMDMwNDA1MDYwNzA4MDkwYTBiMGMwZDBlMGY6QSBuaWNlIGxpdHRsZSBncm91cDow";
+
+	static {
+		try {
+			TEST_TOKEN_VALID = new GroupInviteToken(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
+			TEST_INVITE_MODEL = new GroupInviteModel.Builder()
+				.withGroupName(TEST_GROUP_NAME)
+				.withInviteName(TEST_INVITE_NAME)
+				.withToken(TEST_TOKEN_VALID)
+				.withManualConfirmation(false)
+				.build();
+		} catch (GroupInviteToken.InvalidGroupInviteTokenException | GroupInviteModel.MissingRequiredArgumentsException e) {
+			e.printStackTrace();
+		}
+	}
+
+	private final GroupInviteData TEST_INVITE_DATA = new GroupInviteData(
+		TEST_IDENTITY,
+		TEST_TOKEN_VALID,
+		TEST_GROUP_NAME,
+		TEST_INVITE_TYPE_AUTOMATIC
+	);
+
+	@Before
+	public void setUp() {
+		// create new implementation while only implementing getIdentity with the TEST_IDENTITY because Powermock cannot be used in androidTest scope
+		UserService userService = new UserService() {
+			@Override
+			public void createIdentity(byte[] newRandomSeed) throws Exception {
+
+			}
+
+			@Override
+			public void removeIdentity() throws Exception {
+
+			}
+
+			@Override
+			public Account getAccount() {
+				return null;
+			}
+
+			@Override
+			public Account getAccount(boolean createIfNotExists) {
+				return null;
+			}
+
+			@Override
+			public boolean checkAccount() {
+				return false;
+			}
+
+			@Override
+			public boolean enableAccountAutoSync(boolean enable) {
+				return false;
+			}
+
+			@Override
+			public void removeAccount() {
+
+			}
+
+			@Override
+			public boolean removeAccount(AccountManagerCallback<Boolean> callback) {
+				return false;
+			}
+
+			@Override
+			public boolean hasIdentity() {
+				return false;
+			}
+
+			@Override
+			public String getIdentity() {
+				return TEST_IDENTITY;
+			}
+
+			@Override
+			public boolean isMe(String identity) {
+				return false;
+			}
+
+			@Override
+			public byte[] getPublicKey() {
+				return new byte[0];
+			}
+
+			@Override
+			public byte[] getPrivateKey() {
+				return new byte[0];
+			}
+
+			@Override
+			public String getLinkedEmail() {
+				return null;
+			}
+
+			@Override
+			public String getLinkedMobileE164() {
+				return null;
+			}
+
+			@Override
+			public String getLinkedMobile() {
+				return null;
+			}
+
+			@Override
+			public String getLinkedMobile(boolean returnPendingNumber) {
+				return null;
+			}
+
+			@Override
+			public void linkWithEmail(String email) throws Exception {
+
+			}
+
+			@Override
+			public void unlinkEmail() throws Exception {
+
+			}
+
+			@Override
+			public int getEmailLinkingState() {
+				return 0;
+			}
+
+			@Override
+			public void checkEmailLinkState() {
+
+			}
+
+			@Override
+			public Date linkWithMobileNumber(String number) throws Exception {
+				return null;
+			}
+
+			@Override
+			public void makeMobileLinkCall() throws Exception {
+
+			}
+
+			@Override
+			public void unlinkMobileNumber() throws Exception {
+
+			}
+
+			@Override
+			public boolean verifyMobileNumber(String code) throws Exception {
+				return false;
+			}
+
+			@Override
+			public int getMobileLinkingState() {
+				return 0;
+			}
+
+			@Override
+			public long getMobileLinkingTime() {
+				return 0;
+			}
+
+			@Override
+			public String getPublicNickname() {
+				return null;
+			}
+
+			@Nullable
+			@Override
+			public String setPublicNickname(String publicNickname) {
+				return null;
+			}
+
+			@Override
+			public boolean isTyping(String toIdentity, boolean isTyping) {
+				return false;
+			}
+
+			@Override
+			public boolean restoreIdentity(String backupString, String password) throws Exception {
+				return false;
+			}
+
+			@Override
+			public boolean restoreIdentity(String identity, byte[] privateKey, byte[] publicKey) throws Exception {
+				return false;
+			}
+
+			@Override
+			public void setPolicyResponse(String responseData, String signature, int policyErrorCode) {
+
+			}
+
+			@Override
+			public void setCredentials(LicenseService.Credentials credentials) {
+
+			}
+
+			@Override
+			public boolean sendFlags() {
+				return false;
+			}
+
+			@Override
+			public boolean setRevocationKey(String revocationKey) {
+				return false;
+			}
+
+			@Override
+			public Date getLastRevocationKeySet() {
+				return null;
+			}
+
+			@Override
+			public void checkRevocationKey(boolean force) {
+
+			}
+		};
+		try {
+			this.groupService = ThreemaApplication.getServiceManager().getGroupService();
+		} catch (MasterKeyLockedException | FileSystemNotPresentException e) {
+			e.printStackTrace();
+		}
+		DatabaseServiceNew databaseServiceNew = ThreemaApplication.getServiceManager().getDatabaseServiceNew();
+		this.groupInviteService = new GroupInviteServiceImpl(userService, this.groupService, databaseServiceNew);
+	}
+
+	@Test
+	public void testEncodeDecodeGroupInvite() {
+		Uri encodedGroupInvite = groupInviteService.encodeGroupInviteLink(TEST_INVITE_MODEL);
+
+		Assert.assertEquals("https", encodedGroupInvite.getScheme());
+		Assert.assertEquals(BuildConfig.groupLinkActionUrl, encodedGroupInvite.getAuthority());
+		Assert.assertEquals("/join", encodedGroupInvite.getPath());
+		Assert.assertEquals(TEST_ENCODED_INVITE, encodedGroupInvite.getEncodedFragment());
+	}
+
+	@Test
+	public void testDecodeGroupInvite() throws IOException, GroupInviteToken.InvalidGroupInviteTokenException {
+		GroupInviteData inviteDataFromDecodedUri = groupInviteService.decodeGroupInviteLink(TEST_ENCODED_INVITE);
+
+		Assert.assertEquals(TEST_INVITE_DATA.getAdminIdentity(),  inviteDataFromDecodedUri.getAdminIdentity());
+		Assert.assertEquals(TEST_INVITE_DATA.getToken(), inviteDataFromDecodedUri.getToken());
+		Assert.assertEquals(TEST_INVITE_DATA.getGroupName(), inviteDataFromDecodedUri.getGroupName());
+		Assert.assertEquals(TEST_INVITE_DATA.getInviteType(), inviteDataFromDecodedUri.getInviteType());
+	}
+}

+ 71 - 0
app/src/androidTest/java/ch/threema/app/testutils/CaptureLogcatOnTestFailureRule.java

@@ -0,0 +1,71 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2021 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.app.testutils;
+
+import org.junit.AssumptionViolatedException;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * Capture adb logcat on test failure.
+ *
+ * Based on https://www.braze.com/resources/articles/logcat-junit-android-tests
+ */
+public class CaptureLogcatOnTestFailureRule implements TestRule {
+	private static final String LOGCAT_HEADER = "\n================ Logcat Output ================\n";
+	private static final String STACKTRACE_HEADER = "\n================ Stacktrace ================\n";
+	private static final String ORIGINAL_CLASS_HEADER = "\nOriginal class: ";
+
+	@Override
+	public Statement apply(Statement base, Description description) {
+		return new Statement() {
+			@Override
+			public void evaluate() throws Throwable {
+				// Before test, clear logcat
+				TestHelpers.clearLogcat();
+
+				try {
+					// Run statement
+					base.evaluate();
+				} catch (Throwable originalThrowable) {
+					if (originalThrowable instanceof AssumptionViolatedException) {
+						throw originalThrowable;
+					}
+
+					// Fetch logcat logs
+					final String testName = description.getMethodName() + "(" + description.getClassName() + ")";
+					final String logcatLogs = TestHelpers.getTestLogs(testName);
+
+					// Throw updated throwable
+					final String thrownMessage = originalThrowable.getMessage()
+						+ ORIGINAL_CLASS_HEADER + originalThrowable.getClass().getName()
+						+ LOGCAT_HEADER + logcatLogs
+						+ STACKTRACE_HEADER;
+					final Throwable modifiedThrowable = new Throwable(thrownMessage);
+					modifiedThrowable.setStackTrace(originalThrowable.getStackTrace());
+					throw modifiedThrowable;
+				}
+			}
+		};
+	}
+}

+ 65 - 2
app/src/androidTest/java/ch/threema/app/TestHelpers.java → app/src/androidTest/java/ch/threema/app/testutils/TestHelpers.java

@@ -19,13 +19,17 @@
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
  */
  */
 
 
-package ch.threema.app;
+package ch.threema.app.testutils;
 
 
 import android.app.ActivityManager;
 import android.app.ActivityManager;
 import android.app.ActivityManager.RunningServiceInfo;
 import android.app.ActivityManager.RunningServiceInfo;
 import android.content.Context;
 import android.content.Context;
 import android.util.Log;
 import android.util.Log;
 
 
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.NonNull;
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.BySelector;
 import androidx.test.uiautomator.BySelector;
@@ -33,7 +37,7 @@ import androidx.test.uiautomator.UiDevice;
 import androidx.test.uiautomator.Until;
 import androidx.test.uiautomator.Until;
 import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.services.UserService;
 import ch.threema.app.services.UserService;
-import ch.threema.client.Utils;
+import ch.threema.base.utils.Utils;
 
 
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotNull;
 
 
@@ -90,4 +94,63 @@ public class TestHelpers {
 		Log.i(TAG, "Test identity restored: " + identity);
 		Log.i(TAG, "Test identity restored: " + identity);
 		return identity;
 		return identity;
 	}
 	}
+
+	public static void clearLogcat() {
+		try {
+			Runtime.getRuntime().exec(new String[] { "logcat", "-c" });
+		} catch (IOException e) {
+			Log.e(TAG, "Could not clear logcat", e);
+		}
+	}
+
+	/**
+	 * Return adb logs since the start of the specified test.
+	 *
+	 * Based on https://www.braze.com/resources/articles/logcat-junit-android-tests
+	 */
+	public static String getTestLogs(@NonNull String testName) {
+		final StringBuilder logLines = new StringBuilder();
+
+		// Process id is used to filter messages
+		final String currentProcessId = Integer.toString(android.os.Process.myPid());
+
+		// A snippet of text that uniquely determines where the relevant logs start in the logcat
+		final String testStartMessage = "TestRunner: started: " + testName;
+
+		// When true, write every line from the logcat buffer to the string builder
+		boolean recording = false;
+
+		// Logcat command:
+		//   -d asks the command to completely dump to our buffer, then return
+		//   -v threadtime sets the output log format
+		final String[] command = new String[] { "logcat", "-d", "-v", "threadtime" };
+
+		BufferedReader bufferedReader = null;
+		try {
+			final Process process = Runtime.getRuntime().exec(command);
+			bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
+			String line;
+			while ((line = bufferedReader.readLine()) != null) {
+				if (line.contains(testStartMessage)) {
+					recording = true;
+				}
+				if (recording) {
+					logLines.append(line);
+					logLines.append('\n');
+				}
+			}
+		} catch (IOException e) {
+			Log.e(TAG, "Failed to run logcat command", e);
+		} finally {
+			if (bufferedReader != null) {
+				try {
+					bufferedReader.close();
+				} catch (IOException e) {
+					Log.e(TAG, "Failed to close buffered reader", e);
+				}
+			}
+		}
+
+		return logLines.toString();
+	}
 }
 }

+ 40 - 0
app/src/androidTest/java/ch/threema/app/testutils/ThreemaAssert.java

@@ -0,0 +1,40 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2021 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.app.testutils;
+
+import org.junit.Assert;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Better assertions.
+ */
+public class ThreemaAssert {
+	private final static String START = "=== Start ===\n";
+	private final static String END = "\n=== End ===";
+
+	public static void assertContains(@NonNull String haystack, @NonNull CharSequence needle) {
+		if (!haystack.contains(needle)) {
+			Assert.fail("Substring '" + needle + "' not found in the following string:\n" + START + haystack + END);
+		}
+	}
+}

+ 2 - 2
app/src/androidTest/java/ch/threema/app/utils/BackgroundErrorNotificationTest.java

@@ -48,7 +48,7 @@ import androidx.test.uiautomator.UiObject2;
 import androidx.test.uiautomator.Until;
 import androidx.test.uiautomator.Until;
 import ch.threema.app.R;
 import ch.threema.app.R;
 import ch.threema.app.ScreenshotTakingRule;
 import ch.threema.app.ScreenshotTakingRule;
-import ch.threema.app.TestHelpers;
+import ch.threema.app.testutils.TestHelpers;
 import ch.threema.app.notifications.BackgroundErrorNotification;
 import ch.threema.app.notifications.BackgroundErrorNotification;
 
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertEquals;
@@ -106,7 +106,7 @@ public class BackgroundErrorNotificationTest {
 		TestHelpers.openNotificationArea(mDevice);
 		TestHelpers.openNotificationArea(mDevice);
 
 
 		// Verify notification contents
 		// Verify notification contents
-		final BySelector titleSelector = By.res("android:id/title").text("Error: T1tl3");
+		final BySelector titleSelector = By.res("android:id/title").text(context.getString(R.string.error) + ": T1tl3");
 		final BySelector bodySelector = By.text("The body of the notification");
 		final BySelector bodySelector = By.text("The body of the notification");
 		assertNotNull("Notification title not found", mDevice.wait(Until.findObject(titleSelector), 1000));
 		assertNotNull("Notification title not found", mDevice.wait(Until.findObject(titleSelector), 1000));
 		assertNotNull("Notification text not found", mDevice.wait(Until.findObject(bodySelector), 1000));
 		assertNotNull("Notification text not found", mDevice.wait(Until.findObject(bodySelector), 1000));

+ 1 - 1
app/src/androidTest/java/ch/threema/app/voip/VoipStatusMessageTest.java

@@ -33,7 +33,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.platform.app.InstrumentationRegistry;
 import ch.threema.app.R;
 import ch.threema.app.R;
 import ch.threema.app.utils.MessageUtil;
 import ch.threema.app.utils.MessageUtil;
-import ch.threema.client.voip.VoipCallAnswerData;
+import ch.threema.domain.protocol.csp.messages.voip.VoipCallAnswerData;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.MessageModel;
 import ch.threema.storage.models.MessageModel;
 import ch.threema.storage.models.data.status.VoipStatusDataModel;
 import ch.threema.storage.models.data.status.VoipStatusDataModel;

+ 1 - 1
app/src/androidTest/java/ch/threema/app/webclient/converter/MessageTest.java

@@ -37,7 +37,7 @@ import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.Nullable;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 import androidx.test.filters.SmallTest;
-import ch.threema.client.file.FileData;
+import ch.threema.domain.protocol.csp.messages.file.FileData;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.MessageModel;
 import ch.threema.storage.models.MessageModel;
 import ch.threema.storage.models.data.media.FileDataModel;
 import ch.threema.storage.models.data.media.FileDataModel;

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

@@ -36,7 +36,7 @@ import androidx.work.Worker;
 import androidx.work.WorkerParameters;
 import androidx.work.WorkerParameters;
 import ch.threema.app.utils.PushUtil;
 import ch.threema.app.utils.PushUtil;
 import ch.threema.base.ThreemaException;
 import ch.threema.base.ThreemaException;
-import ch.threema.client.ProtocolDefines;
+import ch.threema.domain.protocol.csp.ProtocolDefines;
 
 
 public class PushRegistrationWorker extends Worker {
 public class PushRegistrationWorker extends Worker {
 	private final Logger logger = LoggerFactory.getLogger(PushRegistrationWorker.class);
 	private final Logger logger = LoggerFactory.getLogger(PushRegistrationWorker.class);

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

@@ -41,7 +41,7 @@ import androidx.annotation.NonNull;
 import ch.threema.app.utils.PushUtil;
 import ch.threema.app.utils.PushUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.base.ThreemaException;
 import ch.threema.base.ThreemaException;
-import ch.threema.client.ProtocolDefines;
+import ch.threema.domain.protocol.csp.ProtocolDefines;
 
 
 public class PushService extends FirebaseMessagingService {
 public class PushService extends FirebaseMessagingService {
 	private static final Logger logger = LoggerFactory.getLogger(PushService.class);
 	private static final Logger logger = LoggerFactory.getLogger(PushService.class);

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

@@ -53,7 +53,7 @@ import ch.threema.app.voip.services.CallRejectService;
 import ch.threema.app.voip.services.VoipCallService;
 import ch.threema.app.voip.services.VoipCallService;
 import ch.threema.app.voip.services.VoipStateService;
 import ch.threema.app.voip.services.VoipStateService;
 import ch.threema.app.voip.util.VoipUtil;
 import ch.threema.app.voip.util.VoipUtil;
-import ch.threema.client.voip.VoipCallAnswerData;
+import ch.threema.domain.protocol.csp.messages.voip.VoipCallAnswerData;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.ContactModel;
 
 
 import static ch.threema.app.voip.services.VoipCallService.EXTRA_CALL_ID;
 import static ch.threema.app.voip.services.VoipCallService.EXTRA_CALL_ID;

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

@@ -36,7 +36,7 @@ import androidx.work.Worker;
 import androidx.work.WorkerParameters;
 import androidx.work.WorkerParameters;
 import ch.threema.app.utils.PushUtil;
 import ch.threema.app.utils.PushUtil;
 import ch.threema.base.ThreemaException;
 import ch.threema.base.ThreemaException;
-import ch.threema.client.ProtocolDefines;
+import ch.threema.domain.protocol.csp.ProtocolDefines;
 
 
 public class PushRegistrationWorker extends Worker {
 public class PushRegistrationWorker extends Worker {
 	private final Logger logger = LoggerFactory.getLogger(PushRegistrationWorker.class);
 	private final Logger logger = LoggerFactory.getLogger(PushRegistrationWorker.class);

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

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

+ 33 - 14
app/src/main/AndroidManifest.xml

@@ -182,11 +182,9 @@
 			android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
 			android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
 			android:windowSoftInputMode="stateUnchanged">
 			android:windowSoftInputMode="stateUnchanged">
 			<intent-filter android:label="@string/threema_contact"
 			<intent-filter android:label="@string/threema_contact"
-			               tools:ignore="AppLinkUrlError">
+					tools:ignore="AppLinkUrlError">
 				<action android:name="android.intent.action.VIEW"/>
 				<action android:name="android.intent.action.VIEW"/>
-
 				<category android:name="android.intent.category.DEFAULT"/>
 				<category android:name="android.intent.category.DEFAULT"/>
-
 				<data android:mimeType="@string/contacts_mime_type"/>
 				<data android:mimeType="@string/contacts_mime_type"/>
 			</intent-filter>
 			</intent-filter>
 		</activity>
 		</activity>
@@ -236,7 +234,7 @@
 			</intent-filter>
 			</intent-filter>
 			<meta-data
 			<meta-data
 				android:name="android.service.chooser.chooser_target_service"
 				android:name="android.service.chooser.chooser_target_service"
-				android:value=".RecipientChooserTargetService"/>
+				android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
 		</activity>
 		</activity>
 		<activity
 		<activity
 			android:name=".activities.RecipientListBaseActivity"
 			android:name=".activities.RecipientListBaseActivity"
@@ -395,6 +393,26 @@
 			android:theme="@style/Theme.Threema.TransparentStatusbar"
 			android:theme="@style/Theme.Threema.TransparentStatusbar"
 			android:configChanges="uiMode"
 			android:configChanges="uiMode"
 			android:windowSoftInputMode="stateHidden" />
 			android:windowSoftInputMode="stateHidden" />
+		<activity
+			android:name=".qrscanner.activity.BaseQrScannerActivity"
+			android:theme="@style/Theme.Threema.Translucent"/>
+		<activity
+			android:name=".grouplinks.AddGroupLinkBottomSheet"
+			android:theme="@style/Theme.Threema.Translucent" />
+		<activity
+			android:name=".grouplinks.GroupLinkQrCodeActivity"
+			android:theme="@style/Theme.Threema.WithToolbar"/>
+		<activity
+			android:name=".grouplinks.GroupLinkOverviewActivity"
+			android:theme="@style/Theme.Threema.WithToolbar"/>
+		<activity
+			android:name=".grouplinks.OutgoingGroupRequestActivity"
+			android:parentActivityName=".activities.HomeActivity"
+			android:theme="@style/Theme.Threema.WithToolbar"/>
+		<activity
+			android:name=".grouplinks.IncomingGroupRequestActivity"
+			android:parentActivityName=".activities.HomeActivity"
+			android:theme="@style/Theme.Threema.WithToolbar"/>
 		<activity
 		<activity
 			android:name=".activities.DistributionListAddActivity"
 			android:name=".activities.DistributionListAddActivity"
 			android:theme="@style/Theme.Threema.WithToolbar"
 			android:theme="@style/Theme.Threema.WithToolbar"
@@ -570,11 +588,16 @@
 			android:screenOrientation="sensorPortrait"
 			android:screenOrientation="sensorPortrait"
 			android:theme="@style/Theme.Threema.Wizard"/>
 			android:theme="@style/Theme.Threema.Wizard"/>
 		<activity
 		<activity
-			android:name=".activities.wizard.WizardRestoreIDActivity"
+			android:name=".activities.wizard.WizardIDRestoreActivity"
 			android:screenOrientation="sensorPortrait"
 			android:screenOrientation="sensorPortrait"
 			android:theme="@style/Theme.Threema.Wizard"/>
 			android:theme="@style/Theme.Threema.Wizard"/>
 		<activity
 		<activity
-			android:name=".activities.wizard.WizardRestoreMainActivity"
+			android:name=".activities.wizard.WizardSafeRestoreActivity"
+			android:screenOrientation="sensorPortrait"
+			android:theme="@style/Theme.Threema.Wizard"
+			android:windowSoftInputMode="stateAlwaysHidden"/>
+		<activity
+			android:name=".activities.wizard.WizardBackupRestoreActivity"
 			android:screenOrientation="sensorPortrait"
 			android:screenOrientation="sensorPortrait"
 			android:theme="@style/Theme.Threema.Wizard"
 			android:theme="@style/Theme.Threema.Wizard"
 			android:windowSoftInputMode="stateAlwaysHidden"/>
 			android:windowSoftInputMode="stateAlwaysHidden"/>
@@ -676,6 +699,10 @@
 					android:scheme="https"
 					android:scheme="https"
 					android:host="${contactActionUrl}"
 					android:host="${contactActionUrl}"
 					android:pathPattern="/.*"/>
 					android:pathPattern="/.*"/>
+				<data
+					android:scheme="https"
+					android:host="${groupLinkActionUrl}"
+					android:pathPattern="/join#\.*"/>
 			</intent-filter>
 			</intent-filter>
 		</activity>
 		</activity>
 		<activity
 		<activity
@@ -777,14 +804,6 @@
 			android:permission="android.permission.BIND_JOB_SERVICE"
 			android:permission="android.permission.BIND_JOB_SERVICE"
 			android:enabled="true"
 			android:enabled="true"
 			android:exported="false"/>
 			android:exported="false"/>
-		<service
-			android:name=".RecipientChooserTargetService"
-			android:label="@string/app_name"
-			android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
-			<intent-filter>
-				<action android:name="android.service.chooser.ChooserTargetService"/>
-			</intent-filter>
-		</service>
 		<service
 		<service
 			android:name=".jobs.ReConnectJobService"
 			android:name=".jobs.ReConnectJobService"
 			android:permission="android.permission.BIND_JOB_SERVICE"/>
 			android:permission="android.permission.BIND_JOB_SERVICE"/>

+ 6 - 29
app/src/main/java/ch/threema/app/BuildFlavor.java

@@ -28,19 +28,17 @@ public class BuildFlavor {
 	private final static String FLAVOR_STORE_GOOGLE_WORK = "store_google_work";
 	private final static String FLAVOR_STORE_GOOGLE_WORK = "store_google_work";
 	private final static String FLAVOR_SANDBOX = "sandbox";
 	private final static String FLAVOR_SANDBOX = "sandbox";
 	private final static String FLAVOR_SANDBOX_WORK = "sandbox_work";
 	private final static String FLAVOR_SANDBOX_WORK = "sandbox_work";
+	private final static String FLAVOR_ONPREM = "onprem";
 	private final static String FLAVOR_RED = "red";
 	private final static String FLAVOR_RED = "red";
 	private final static String FLAVOR_HMS = "hms";
 	private final static String FLAVOR_HMS = "hms";
 	private final static String FLAVOR_HMS_WORK = "hms_work";
 	private final static String FLAVOR_HMS_WORK = "hms_work";
 
 
 	public enum LicenseType {
 	public enum LicenseType {
-		NONE, GOOGLE, SERIAL, GOOGLE_WORK, HMS, HMS_WORK
+		NONE, GOOGLE, SERIAL, GOOGLE_WORK, HMS, HMS_WORK, ONPREM
 	}
 	}
 
 
 	private static boolean initialized = false;
 	private static boolean initialized = false;
 	private static LicenseType licenseType = null;
 	private static LicenseType licenseType = null;
-	private static int serverPort = 5222;
-	private static int serverPortAlt = 443;
-	private static boolean sandbox = false;
 	private static String name = null;
 	private static String name = null;
 
 
 	/**
 	/**
@@ -52,33 +50,11 @@ public class BuildFlavor {
 		return licenseType;
 		return licenseType;
 	}
 	}
 
 
-	/**
-	 * Server Port
-	 * @return
-	 */
-	public static int getServerPort() {
-		init();
-		return serverPort;
-	}
-	/**
-	 * Server Port
-	 * @return
-	 */
-	public static int getServerPortAlt() {
-		init();
-		return serverPortAlt;
-	}
-
 	public static String getName() {
 	public static String getName() {
 		init();
 		init();
 		return name;
 		return name;
 	}
 	}
 
 
-	public static boolean isSandbox() {
-		init();
-		return sandbox;
-	}
-
 	private static void init() {
 	private static void init() {
 		if(!initialized) {
 		if(!initialized) {
 
 
@@ -100,17 +76,18 @@ public class BuildFlavor {
 					name = "Work";
 					name = "Work";
 					break;
 					break;
 				case FLAVOR_SANDBOX:
 				case FLAVOR_SANDBOX:
-					sandbox = true;
 					name = "Sandbox";
 					name = "Sandbox";
 					licenseType = LicenseType.NONE;
 					licenseType = LicenseType.NONE;
 					break;
 					break;
 				case FLAVOR_SANDBOX_WORK:
 				case FLAVOR_SANDBOX_WORK:
-					sandbox = true;
 					name = "Sandbox Work";
 					name = "Sandbox Work";
 					licenseType = LicenseType.GOOGLE_WORK;
 					licenseType = LicenseType.GOOGLE_WORK;
 					break;
 					break;
+				case FLAVOR_ONPREM:
+					name = "OnPrem";
+					licenseType = LicenseType.ONPREM;
+					break;
 				case FLAVOR_RED:
 				case FLAVOR_RED:
-					sandbox = true;
 					name = "Red";
 					name = "Red";
 					licenseType = LicenseType.GOOGLE_WORK;
 					licenseType = LicenseType.GOOGLE_WORK;
 					break;
 					break;

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

@@ -28,9 +28,7 @@ import android.content.pm.ProviderInfo;
 import android.content.res.XmlResourceParser;
 import android.content.res.XmlResourceParser;
 import android.database.Cursor;
 import android.database.Cursor;
 import android.database.MatrixCursor;
 import android.database.MatrixCursor;
-
 import android.net.Uri;
 import android.net.Uri;
-import android.os.Build;
 import android.os.Environment;
 import android.os.Environment;
 import android.provider.OpenableColumns;
 import android.provider.OpenableColumns;
 import android.text.TextUtils;
 import android.text.TextUtils;
@@ -317,8 +315,7 @@ public class NamedFileProvider extends FileProvider {
 					if (externalCacheDirs.length > 0) {
 					if (externalCacheDirs.length > 0) {
 						target = externalCacheDirs[0];
 						target = externalCacheDirs[0];
 					}
 					}
-				} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
-					&& TAG_EXTERNAL_MEDIA.equals(tag)) {
+				} else if (TAG_EXTERNAL_MEDIA.equals(tag)) {
 					File[] externalMediaDirs = context.getExternalMediaDirs();
 					File[] externalMediaDirs = context.getExternalMediaDirs();
 					if (externalMediaDirs.length > 0) {
 					if (externalMediaDirs.length > 0) {
 						target = externalMediaDirs[0];
 						target = externalMediaDirs[0];

+ 0 - 147
app/src/main/java/ch/threema/app/RecipientChooserTargetService.java

@@ -1,147 +0,0 @@
-/*  _____ _
- * |_   _| |_  _ _ ___ ___ _ __  __ _
- *   | | | ' \| '_/ -_) -_) '  \/ _` |_
- *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
- *
- * Threema for Android
- * Copyright (c) 2016-2021 Threema GmbH
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package ch.threema.app;
-
-import android.annotation.TargetApi;
-import android.content.ComponentName;
-import android.content.IntentFilter;
-import android.graphics.Bitmap;
-import android.graphics.drawable.Icon;
-import android.os.Build;
-import android.os.Bundle;
-import android.service.chooser.ChooserTarget;
-import android.service.chooser.ChooserTargetService;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-import androidx.annotation.Nullable;
-import ch.threema.app.activities.RecipientListActivity;
-import ch.threema.app.managers.ServiceManager;
-import ch.threema.app.services.ContactService;
-import ch.threema.app.services.ConversationService;
-import ch.threema.app.services.GroupService;
-import ch.threema.app.services.PreferenceService;
-import ch.threema.app.utils.IntentDataUtil;
-import ch.threema.app.utils.NameUtil;
-import ch.threema.base.ThreemaException;
-import ch.threema.storage.models.ConversationModel;
-
-@TargetApi(Build.VERSION_CODES.M)
-public class RecipientChooserTargetService extends ChooserTargetService {
-	private static final Logger logger = LoggerFactory.getLogger(RecipientChooserTargetService.class);
-
-	private static final int MAX_CONVERSATIONS = 8;
-	private PreferenceService preferenceService;
-
-	@Override
-	public @Nullable List<ChooserTarget> onGetChooserTargets(
-		ComponentName targetActivityName,
-		IntentFilter matchedFilter
-	) {
-		logger.debug("onGetChooserTargets");
-
-		final ServiceManager serviceManager = ThreemaApplication.getServiceManager();
-		if (serviceManager == null) {
-			return null;
-		}
-
-		ConversationService conversationService = null;
-		GroupService groupService = null;
-		ContactService contactService = null;
-		preferenceService = null;
-
-		try {
-			conversationService = serviceManager.getConversationService();
-			groupService = serviceManager.getGroupService();
-			contactService = serviceManager.getContactService();
-			preferenceService = serviceManager.getPreferenceService();
-		} catch (ThreemaException e) {
-			logger.error("Exception", e);
-		}
-
-		if (conversationService == null || groupService == null || contactService == null || preferenceService == null) {
-			return null;
-		}
-
-		if (!preferenceService.isDirectShare()) {
-			// only enable this feature if sync contacts is enabled (privacy risk)
-			return null;
-		}
-
-		final ConversationService.Filter filter = new ConversationService.Filter() {
-			@Override
-			public boolean onlyUnread() {
-				return false;
-			}
-
-			@Override
-			public boolean noDistributionLists() {
-				return true;
-			}
-
-			@Override
-			public boolean noHiddenChats() { return preferenceService.isPrivateChatsHidden(); }
-
-			@Override
-			public boolean noInvalid() {
-				return true;
-			}
-		};
-
-		final List<ConversationModel> conversations = conversationService.getAll(false, filter);
-		int length = Math.min(conversations.size(), MAX_CONVERSATIONS);
-
-		final ComponentName componentName = new ComponentName(
-			getPackageName(),
-			Objects.requireNonNull(RecipientListActivity.class.getCanonicalName())
-		);
-
-		final List<ChooserTarget> targets = new ArrayList<>();
-		for (int i = 0; i < length; i++) {
-			final String title;
-			final Bitmap avatar;
-			final Bundle extras = new Bundle();
-			final ConversationModel conversationModel = conversations.get(i);
-
-			if (conversationModel.isGroupConversation()) {
-				title = NameUtil.getDisplayName(conversationModel.getGroup(), groupService);
-				avatar = groupService.getAvatar(conversationModel.getGroup(), false);
-				extras.putInt(IntentDataUtil.INTENT_DATA_GROUP_ID, conversationModel.getGroup().getId());
-			} else {
-				title = NameUtil.getDisplayNameOrNickname(conversationModel.getContact(), true);
-				avatar = contactService.getAvatar(conversationModel.getContact(), false);
-				extras.putString(IntentDataUtil.INTENT_DATA_IDENTITY, conversationModel.getContact().getIdentity());
-			}
-
-			final Icon icon = Icon.createWithBitmap(avatar);
-			final float score = ((float) MAX_CONVERSATIONS - ((float) i / 2)) / (float) MAX_CONVERSATIONS;
-
-			targets.add(new ChooserTarget(title, icon, score, componentName, extras));
-		}
-		return targets;
-	}
-}

+ 105 - 53
app/src/main/java/ch/threema/app/ThreemaApplication.java

@@ -24,9 +24,7 @@ package ch.threema.app;
 import android.annotation.SuppressLint;
 import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
 import android.annotation.TargetApi;
 import android.app.Activity;
 import android.app.Activity;
-import android.app.AlarmManager;
 import android.app.NotificationManager;
 import android.app.NotificationManager;
-import android.app.PendingIntent;
 import android.app.job.JobInfo;
 import android.app.job.JobInfo;
 import android.app.job.JobScheduler;
 import android.app.job.JobScheduler;
 import android.content.BroadcastReceiver;
 import android.content.BroadcastReceiver;
@@ -42,7 +40,6 @@ import android.os.Build;
 import android.os.Environment;
 import android.os.Environment;
 import android.os.PowerManager;
 import android.os.PowerManager;
 import android.os.StrictMode;
 import android.os.StrictMode;
-import android.os.SystemClock;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract;
 import android.text.format.DateUtils;
 import android.text.format.DateUtils;
 import android.widget.Toast;
 import android.widget.Toast;
@@ -93,6 +90,7 @@ import androidx.work.WorkManager;
 import ch.threema.app.backuprestore.csv.BackupService;
 import ch.threema.app.backuprestore.csv.BackupService;
 import ch.threema.app.exceptions.DatabaseMigrationFailedException;
 import ch.threema.app.exceptions.DatabaseMigrationFailedException;
 import ch.threema.app.exceptions.FileSystemNotPresentException;
 import ch.threema.app.exceptions.FileSystemNotPresentException;
+import ch.threema.app.grouplinks.IncomingGroupJoinRequestListener;
 import ch.threema.app.jobs.WorkSyncJobService;
 import ch.threema.app.jobs.WorkSyncJobService;
 import ch.threema.app.jobs.WorkSyncService;
 import ch.threema.app.jobs.WorkSyncService;
 import ch.threema.app.listeners.BallotVoteListener;
 import ch.threema.app.listeners.BallotVoteListener;
@@ -127,6 +125,7 @@ import ch.threema.app.services.MessageService;
 import ch.threema.app.services.MessageServiceImpl;
 import ch.threema.app.services.MessageServiceImpl;
 import ch.threema.app.services.NotificationService;
 import ch.threema.app.services.NotificationService;
 import ch.threema.app.services.PreferenceService;
 import ch.threema.app.services.PreferenceService;
+import ch.threema.app.services.ShortcutService;
 import ch.threema.app.services.SynchronizeContactsService;
 import ch.threema.app.services.SynchronizeContactsService;
 import ch.threema.app.services.UpdateSystemService;
 import ch.threema.app.services.UpdateSystemService;
 import ch.threema.app.services.UpdateSystemServiceImpl;
 import ch.threema.app.services.UpdateSystemServiceImpl;
@@ -158,12 +157,12 @@ import ch.threema.app.webclient.services.instance.DisconnectContext;
 import ch.threema.app.webclient.state.WebClientSessionState;
 import ch.threema.app.webclient.state.WebClientSessionState;
 import ch.threema.app.workers.IdentityStatesWorker;
 import ch.threema.app.workers.IdentityStatesWorker;
 import ch.threema.base.ThreemaException;
 import ch.threema.base.ThreemaException;
-import ch.threema.client.AppVersion;
-import ch.threema.client.ConnectionState;
-import ch.threema.client.ConnectionStateListener;
-import ch.threema.client.NonceFactory;
-import ch.threema.client.ThreemaConnection;
-import ch.threema.client.Utils;
+import ch.threema.base.crypto.NonceFactory;
+import ch.threema.base.utils.Utils;
+import ch.threema.domain.models.AppVersion;
+import ch.threema.domain.protocol.csp.connection.ConnectionState;
+import ch.threema.domain.protocol.csp.connection.ConnectionStateListener;
+import ch.threema.domain.protocol.csp.connection.ThreemaConnection;
 import ch.threema.localcrypto.MasterKey;
 import ch.threema.localcrypto.MasterKey;
 import ch.threema.localcrypto.MasterKeyLockedException;
 import ch.threema.localcrypto.MasterKeyLockedException;
 import ch.threema.logging.backend.DebugLogFileBackend;
 import ch.threema.logging.backend.DebugLogFileBackend;
@@ -182,11 +181,14 @@ import ch.threema.storage.models.ballot.GroupBallotModel;
 import ch.threema.storage.models.ballot.IdentityBallotModel;
 import ch.threema.storage.models.ballot.IdentityBallotModel;
 import ch.threema.storage.models.ballot.LinkBallotModel;
 import ch.threema.storage.models.ballot.LinkBallotModel;
 import ch.threema.storage.models.data.status.VoipStatusDataModel;
 import ch.threema.storage.models.data.status.VoipStatusDataModel;
+import ch.threema.storage.models.group.IncomingGroupJoinRequestModel;
 
 
 import static android.app.NotificationManager.ACTION_NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED;
 import static android.app.NotificationManager.ACTION_NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED;
 import static android.app.NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED;
 import static android.app.NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED;
 import static android.app.NotificationManager.EXTRA_BLOCKED_STATE;
 import static android.app.NotificationManager.EXTRA_BLOCKED_STATE;
 import static android.app.NotificationManager.EXTRA_NOTIFICATION_CHANNEL_GROUP_ID;
 import static android.app.NotificationManager.EXTRA_NOTIFICATION_CHANNEL_GROUP_ID;
+import static ch.threema.app.services.PreferenceService.Theme_DARK;
+import static ch.threema.app.services.PreferenceService.Theme_LIGHT;
 
 
 public class ThreemaApplication extends MultiDexApplication implements DefaultLifecycleObserver {
 public class ThreemaApplication extends MultiDexApplication implements DefaultLifecycleObserver {
 
 
@@ -202,11 +204,14 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 	public static final String INTENT_DATA_TIMESTAMP = "timestamp";
 	public static final String INTENT_DATA_TIMESTAMP = "timestamp";
 	public static final String INTENT_DATA_EDITFOCUS = "editfocus";
 	public static final String INTENT_DATA_EDITFOCUS = "editfocus";
 	public static final String INTENT_DATA_GROUP = "group";
 	public static final String INTENT_DATA_GROUP = "group";
+	public static final String INTENT_DATA_GROUP_LINK = "group_link";
 	public static final String INTENT_DATA_DISTRIBUTION_LIST = "distribution_list";
 	public static final String INTENT_DATA_DISTRIBUTION_LIST = "distribution_list";
 	public static final String INTENT_DATA_ARCHIVE_FILTER = "archiveFilter";
 	public static final String INTENT_DATA_ARCHIVE_FILTER = "archiveFilter";
 	public static final String INTENT_DATA_QRCODE = "qrcodestring";
 	public static final String INTENT_DATA_QRCODE = "qrcodestring";
 	public static final String INTENT_DATA_QRCODE_TYPE_OK = "qrcodetypeok";
 	public static final String INTENT_DATA_QRCODE_TYPE_OK = "qrcodetypeok";
 	public static final String INTENT_DATA_MESSAGE_ID = "messageid";
 	public static final String INTENT_DATA_MESSAGE_ID = "messageid";
+	public static final String INTENT_DATA_INCOMING_GROUP_REQUEST = "groupRequest";
+	public static final String INTENT_DATA_GROUP_REQUEST_NOTIFICATION_ID = "groupRequestNotificationId";
 	public static final String EXTRA_VOICE_REPLY = "voicereply";
 	public static final String EXTRA_VOICE_REPLY = "voicereply";
 	public static final String EXTRA_OUTPUT_FILE = "output";
 	public static final String EXTRA_OUTPUT_FILE = "output";
 	public static final String EXTRA_ORIENTATION = "rotate";
 	public static final String EXTRA_ORIENTATION = "rotate";
@@ -238,6 +243,8 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 	public static final int WEB_RESUME_FAILED_NOTIFICATION_ID = 737;
 	public static final int WEB_RESUME_FAILED_NOTIFICATION_ID = 737;
 	public static final int PASSPHRASE_SERVICE_NOTIFICATION_ID = 587;
 	public static final int PASSPHRASE_SERVICE_NOTIFICATION_ID = 587;
 	public static final int INCOMING_CALL_NOTIFICATION_ID = 800;
 	public static final int INCOMING_CALL_NOTIFICATION_ID = 800;
+	public static final int GROUP_RESPONSE_NOTIFICATION_ID = 801;
+	public static final int GROUP_REQUEST_NOTIFICATION_ID = 802;
 
 
 	private static final String THREEMA_APPLICATION_LISTENER_TAG = "al";
 	private static final String THREEMA_APPLICATION_LISTENER_TAG = "al";
 	public static final String AES_KEY_FILE = "key.dat";
 	public static final String AES_KEY_FILE = "key.dat";
@@ -246,7 +253,7 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 	public static final String EMAIL_LINKED_PLACEHOLDER = "***@***";
 	public static final String EMAIL_LINKED_PLACEHOLDER = "***@***";
 
 
 	public static final long ACTIVITY_CONNECTION_LIFETIME = 60000;
 	public static final long ACTIVITY_CONNECTION_LIFETIME = 60000;
-	public static final int MAX_BLOB_SIZE_MB = 50;
+	public static final int MAX_BLOB_SIZE_MB = 100;
 	public static final int MAX_BLOB_SIZE = MAX_BLOB_SIZE_MB * 1024 * 1024;
 	public static final int MAX_BLOB_SIZE = MAX_BLOB_SIZE_MB * 1024 * 1024;
 	public static final int MIN_PIN_LENGTH = 4;
 	public static final int MIN_PIN_LENGTH = 4;
 	public static final int MAX_PIN_LENGTH = 8;
 	public static final int MAX_PIN_LENGTH = 8;
@@ -537,7 +544,7 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 				LocalBroadcastManager.getInstance(context).registerReceiver(receiver, new IntentFilter(BackgroundReporter.REPORT_VALIDATION_EVENT));
 				LocalBroadcastManager.getInstance(context).registerReceiver(receiver, new IntentFilter(BackgroundReporter.REPORT_VALIDATION_EVENT));
 
 
 				// register a broadcast receiver for changes in app restrictions
 				// register a broadcast receiver for changes in app restrictions
-				if (ConfigUtils.isWorkRestricted() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+				if (ConfigUtils.isWorkRestricted()) {
 					getAppContext().registerReceiver(new BroadcastReceiver() {
 					getAppContext().registerReceiver(new BroadcastReceiver() {
 						@Override
 						@Override
 						public void onReceive(Context context, Intent intent) {
 						public void onReceive(Context context, Intent intent) {
@@ -548,18 +555,8 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 						}
 						}
 					}, new IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED));
 					}, new IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED));
 				}
 				}
-
-				// setup locale override
-				try {
-					if (getServiceManager() != null) {
-						ConfigUtils.setLocaleOverride(this, getServiceManager().getPreferenceService());
-					}
-				} catch (Exception e) {
-					logger.error("Exception", e);
-				}
 			}
 			}
 		}
 		}
-
 	}
 	}
 
 
 	@Override
 	@Override
@@ -747,6 +744,17 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 		} catch (Exception e) {
 		} catch (Exception e) {
 			logger.error("Exception", e);
 			logger.error("Exception", e);
 		}
 		}
+
+		// set default theme depending on app type
+		if (prefs != null) {
+			if (TestUtil.empty(prefs.getString(getAppContext().getString(R.string.preferences__theme), null))) {
+				prefs.edit().putString(getAppContext().getString(R.string.preferences__theme), String.valueOf(
+					ConfigUtils.isWorkBuild() ?
+						Theme_DARK:
+						Theme_LIGHT)
+				).apply();
+			}
+		}
 	}
 	}
 
 
 	private static void setupLogging(PreferenceStore preferenceStore) {
 	private static void setupLogging(PreferenceStore preferenceStore) {
@@ -771,7 +779,6 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 		}
 		}
 	}
 	}
 
 
-	@TargetApi(Build.VERSION_CODES.LOLLIPOP)
 	public static synchronized void reset() {
 	public static synchronized void reset() {
 
 
 		//set default preferences
 		//set default preferences
@@ -843,15 +850,8 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 			final ThreemaConnection connection = new ThreemaConnection(
 			final ThreemaConnection connection = new ThreemaConnection(
 					identityStore,
 					identityStore,
 					new NonceFactory(nonceDatabaseBlobService),
 					new NonceFactory(nonceDatabaseBlobService),
-					BuildConfig.CHAT_SERVER_PREFIX,
-					BuildConfig.CHAT_SERVER_IPV6_PREFIX,
-					BuildConfig.CHAT_SERVER_SUFFIX,
-					BuildFlavor.getServerPort(),
-					BuildFlavor.getServerPortAlt(),
-					getIPv6(),
-					BuildConfig.SERVER_PUBKEY,
-					BuildConfig.SERVER_PUBKEY_ALT,
-					BuildConfig.CHAT_SERVER_GROUPS);
+					null,
+					getIPv6());
 
 
 			connection.setVersion(appVersion);
 			connection.setVersion(appVersion);
 
 
@@ -881,6 +881,8 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 					updateSystemService
 					updateSystemService
 			);
 			);
 
 
+			connection.setServerAddressProvider(serviceManager.getServerAddressProviderService().getServerAddressProvider());
+
 			// get application restrictions
 			// get application restrictions
 			if (ConfigUtils.isWorkBuild()) {
 			if (ConfigUtils.isWorkBuild()) {
 				AppRestrictionService.getInstance()
 				AppRestrictionService.getInstance()
@@ -951,6 +953,25 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 			}).start();
 			}).start();
 
 
 			initMapbox();
 			initMapbox();
+
+			// publish most recent chats or pinned shortcuts as sharing targets
+			ShortcutService shortcutService;
+			try {
+				shortcutService = serviceManager.getShortcutService();
+
+				if (shortcutService != null) {
+					if (serviceManager.getPreferenceService().isDirectShare()) {
+						shortcutService.publishRecentChatsAsSharingTargets();
+					} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
+						shortcutService.publishPinnedShortcutsAsSharingTargets();
+					}
+				}
+			} catch (ThreemaException e) {
+				logger.error("Exception, failed to publish sharing shortcut targets", e);
+			}
+
+			// setup locale override
+			ConfigUtils.setLocaleOverride(getAppContext(), serviceManager.getPreferenceService());
 		} catch (MasterKeyLockedException e) {
 		} catch (MasterKeyLockedException e) {
 			logger.error("Exception", e);
 			logger.error("Exception", e);
 		} catch (SQLiteException e) {
 		} catch (SQLiteException e) {
@@ -1051,25 +1072,14 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 		logger.info("Scheduling Work Sync. Schedule period: {}", schedulePeriod);
 		logger.info("Scheduling Work Sync. Schedule period: {}", schedulePeriod);
 
 
 		// schedule the start of the service according to schedule period
 		// schedule the start of the service according to schedule period
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-			JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
-			if (jobScheduler != null) {
-				ComponentName serviceComponent = new ComponentName(context, WorkSyncJobService.class);
-				JobInfo.Builder builder = new JobInfo.Builder(WORK_SYNC_JOB_ID, serviceComponent)
-					.setPeriodic(schedulePeriod)
-					.setRequiredNetworkType(android.app.job.JobInfo.NETWORK_TYPE_ANY);
-				jobScheduler.schedule(builder.build());
-				return true;
-			}
-		} else {
-			AlarmManager alarmMgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
-			if (alarmMgr != null) {
-				Intent intent = new Intent(context, WorkSyncService.class);
-				PendingIntent pendingIntent = PendingIntent.getService(context, WORK_SYNC_JOB_ID, intent, PendingIntent.FLAG_CANCEL_CURRENT);
-				alarmMgr.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime(),
-					schedulePeriod, pendingIntent);
-				return true;
-			}
+		JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+		if (jobScheduler != null) {
+			ComponentName serviceComponent = new ComponentName(context, WorkSyncJobService.class);
+			JobInfo.Builder builder = new JobInfo.Builder(WORK_SYNC_JOB_ID, serviceComponent)
+				.setPeriodic(schedulePeriod)
+				.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
+			jobScheduler.schedule(builder.build());
+			return true;
 		}
 		}
 		logger.debug("unable to schedule work sync");
 		logger.debug("unable to schedule work sync");
 		return false;
 		return false;
@@ -1118,8 +1128,11 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 			@Override
 			@Override
 			public void onRemove(GroupModel groupModel) {
 			public void onRemove(GroupModel groupModel) {
 				try {
 				try {
+					final MessageReceiver receiver = serviceManager.getGroupService().createReceiver(groupModel);
+					serviceManager.getBallotService().remove(receiver);
 					serviceManager.getConversationService().removed(groupModel);
 					serviceManager.getConversationService().removed(groupModel);
-					serviceManager.getNotificationService().cancel(new GroupMessageReceiver(groupModel, null, null, null, null));
+					serviceManager.getNotificationService().cancel(new GroupMessageReceiver(groupModel, null, null, null, null, serviceManager.getApiService()));
+					serviceManager.getShortcutService().deleteShortcut(groupModel);
 				} catch (ThreemaException e) {
 				} catch (ThreemaException e) {
 					logger.error("Exception", e);
 					logger.error("Exception", e);
 				}
 				}
@@ -1264,6 +1277,7 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 			public void onLeave(GroupModel groupModel) {
 			public void onLeave(GroupModel groupModel) {
 				try {
 				try {
 					serviceManager.getConversationService().refresh(groupModel);
 					serviceManager.getConversationService().refresh(groupModel);
+					serviceManager.getShortcutService().deleteShortcut(groupModel);
 				} catch (ThreemaException e) {
 				} catch (ThreemaException e) {
 					logger.error("Exception", e);
 					logger.error("Exception", e);
 				}
 				}
@@ -1302,7 +1316,7 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 			public void onRemove(DistributionListModel distributionListModel) {
 			public void onRemove(DistributionListModel distributionListModel) {
 				try {
 				try {
 					serviceManager.getConversationService().removed(distributionListModel);
 					serviceManager.getConversationService().removed(distributionListModel);
-
+					serviceManager.getShortcutService().deleteShortcut(distributionListModel);
 				} catch (ThreemaException e) {
 				} catch (ThreemaException e) {
 					logger.error("Exception", e);
 					logger.error("Exception", e);
 				}
 				}
@@ -1350,6 +1364,20 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 				}
 				}
 			}
 			}
 
 
+			@Override
+			public void onRemoved(List<AbstractMessageModel> removedMessageModels) {
+				logger.debug("MessageListener.onRemoved multi");
+				for (final AbstractMessageModel removedMessageModel : removedMessageModels) {
+					if (!removedMessageModel.isStatusMessage()) {
+						try {
+							serviceManager.getConversationService().refreshWithDeletedMessage(removedMessageModel);
+						} catch (ThreemaException e) {
+							logger.error("Exception", e);
+						}
+					}
+				}
+			}
+
 			@Override
 			@Override
 			public void onProgressChanged(AbstractMessageModel messageModel, int newProgress) {
 			public void onProgressChanged(AbstractMessageModel messageModel, int newProgress) {
 				//ingore
 				//ingore
@@ -1390,6 +1418,29 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 			}
 			}
 		}, THREEMA_APPLICATION_LISTENER_TAG);
 		}, THREEMA_APPLICATION_LISTENER_TAG);
 
 
+		ListenerManager.groupJoinResponseListener.add((outgoingGroupJoinRequestModel, status) -> {
+			NotificationService n = serviceManager.getNotificationService();
+			if (n != null) {
+				n.showGroupJoinResponseNotification(outgoingGroupJoinRequestModel, status, serviceManager.getDatabaseServiceNew());
+			}
+		});
+
+
+		ListenerManager.incomingGroupJoinRequestListener.add(new IncomingGroupJoinRequestListener() {
+			@Override
+			public void onReceived(IncomingGroupJoinRequestModel incomingGroupJoinRequestModel, GroupModel groupModel) {
+				NotificationService n = serviceManager.getNotificationService();
+				if (n != null) {
+					n.showGroupJoinRequestNotification(incomingGroupJoinRequestModel, groupModel);
+				}
+			}
+
+			@Override
+			public void onRespond() {
+				// don't bother here
+			}
+		});
+
 		ListenerManager.serverMessageListeners.add(new ServerMessageListener() {
 		ListenerManager.serverMessageListeners.add(new ServerMessageListener() {
 			@Override
 			@Override
 			public void onAlert(ServerMessageModel serverMessage) {
 			public void onAlert(ServerMessageModel serverMessage) {
@@ -1426,6 +1477,7 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 			public void onRemoved(ContactModel removedContactModel) {
 			public void onRemoved(ContactModel removedContactModel) {
 				try {
 				try {
 					serviceManager.getConversationService().removed(removedContactModel);
 					serviceManager.getConversationService().removed(removedContactModel);
+					serviceManager.getShortcutService().deleteShortcut(removedContactModel);
 
 
 					//remove notification from this contact
 					//remove notification from this contact
 
 
@@ -1434,7 +1486,7 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 							(
 							(
 									removedContactModel,
 									removedContactModel,
 									serviceManager.getContactService(),
 									serviceManager.getContactService(),
-									null, null, null, null));
+									null, null, null, null, serviceManager.getApiService()));
 
 
 					//remove custom avatar (ANDR-353)
 					//remove custom avatar (ANDR-353)
 					FileService f = serviceManager.getFileService();
 					FileService f = serviceManager.getFileService();

+ 19 - 6
app/src/main/java/ch/threema/app/actions/LocationMessageSendAction.java

@@ -23,6 +23,10 @@ package ch.threema.app.actions;
 
 
 import android.location.Location;
 import android.location.Location;
 
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import androidx.annotation.NonNull;
 import ch.threema.app.R;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.messagereceiver.MessageReceiver;
 import ch.threema.app.messagereceiver.MessageReceiver;
@@ -32,6 +36,8 @@ import ch.threema.base.ThreemaException;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.AbstractMessageModel;
 
 
 public class LocationMessageSendAction extends SendAction {
 public class LocationMessageSendAction extends SendAction {
+	private static final Logger logger = LoggerFactory.getLogger(LocationMessageSendAction.class);
+
 	protected static volatile LocationMessageSendAction instance;
 	protected static volatile LocationMessageSendAction instance;
 	private static final Object instanceLock = new Object();
 	private static final Object instanceLock = new Object();
 
 
@@ -52,11 +58,12 @@ public class LocationMessageSendAction extends SendAction {
 		return instance;
 		return instance;
 	}
 	}
 
 
-	public boolean sendLocationMessage(final MessageReceiver[] allReceivers,
-									   final Location location,
-									   final String poiName,
-									   final ActionHandler actionHandler) {
-
+	public boolean sendLocationMessage(
+		final MessageReceiver[] allReceivers,
+		final Location location,
+		final String poiName,
+		final ActionHandler actionHandler
+	) {
 		if (actionHandler == null) {
 		if (actionHandler == null) {
 			return false;
 			return false;
 		}
 		}
@@ -114,7 +121,12 @@ public class LocationMessageSendAction extends SendAction {
 		return true;
 		return true;
 	}
 	}
 
 
-	private void sendSingleMessage(final MessageReceiver messageReceiver, final Location location, final String poiName, final ActionHandler actionHandler) {
+	private void sendSingleMessage(
+		final MessageReceiver messageReceiver,
+		final @NonNull Location location,
+		final String poiName,
+		final @NonNull ActionHandler actionHandler
+	) {
 		if (messageReceiver == null) {
 		if (messageReceiver == null) {
 			actionHandler.onError("No receiver");
 			actionHandler.onError("No receiver");
 			return;
 			return;
@@ -140,6 +152,7 @@ public class LocationMessageSendAction extends SendAction {
 						}
 						}
 					});
 					});
 		} catch (final Exception e) {
 		} catch (final Exception e) {
+			logger.error("Could not send location message", e);
 			actionHandler.onError(e.getMessage());
 			actionHandler.onError(e.getMessage());
 		}
 		}
 	}
 	}

+ 12 - 5
app/src/main/java/ch/threema/app/actions/TextMessageSendAction.java

@@ -21,7 +21,9 @@
 
 
 package ch.threema.app.actions;
 package ch.threema.app.actions;
 
 
-import java.io.UnsupportedEncodingException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.util.ArrayList;
 import java.util.ArrayList;
 
 
 import ch.threema.app.messagereceiver.MessageReceiver;
 import ch.threema.app.messagereceiver.MessageReceiver;
@@ -30,9 +32,11 @@ import ch.threema.app.utils.MessageUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.TextUtil;
 import ch.threema.app.utils.TextUtil;
 import ch.threema.base.ThreemaException;
 import ch.threema.base.ThreemaException;
-import ch.threema.client.ProtocolDefines;
+import ch.threema.domain.protocol.csp.ProtocolDefines;
 
 
 public class TextMessageSendAction extends SendAction {
 public class TextMessageSendAction extends SendAction {
+	private static final Logger logger = LoggerFactory.getLogger(TextMessageSendAction.class);
+
 	protected static volatile TextMessageSendAction instance;
 	protected static volatile TextMessageSendAction instance;
 	private static final Object instanceLock = new Object();
 	private static final Object instanceLock = new Object();
 
 
@@ -51,9 +55,11 @@ public class TextMessageSendAction extends SendAction {
 		return instance;
 		return instance;
 	}
 	}
 
 
-	public boolean sendTextMessage(final MessageReceiver[] allReceivers,
-	                               String text,
-	                               final ActionHandler actionHandler) {
+	public boolean sendTextMessage(
+		final MessageReceiver[] allReceivers,
+		String text,
+		final ActionHandler actionHandler
+	) {
 
 
 		if (actionHandler == null) {
 		if (actionHandler == null) {
 			return false;
 			return false;
@@ -93,6 +99,7 @@ public class TextMessageSendAction extends SendAction {
 						messageService.sendText(messageText, receiver);
 						messageService.sendText(messageText, receiver);
 					}
 					}
 				} catch (final Exception e) {
 				} catch (final Exception e) {
+					logger.error("Could not send text message", e);
 					actionHandler.onError(e.getMessage());
 					actionHandler.onError(e.getMessage());
 					return false;
 					return false;
 				}
 				}

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

@@ -36,7 +36,6 @@ import android.widget.Toast;
 import com.google.android.material.snackbar.BaseTransientBottomBar;
 import com.google.android.material.snackbar.BaseTransientBottomBar;
 import com.google.android.material.snackbar.Snackbar;
 import com.google.android.material.snackbar.Snackbar;
 
 
-import java.io.IOException;
 import java.util.Date;
 import java.util.Date;
 
 
 import androidx.annotation.NonNull;
 import androidx.annotation.NonNull;
@@ -58,17 +57,13 @@ import ch.threema.app.services.QRCodeService;
 import ch.threema.app.utils.AppRestrictionUtil;
 import ch.threema.app.utils.AppRestrictionUtil;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.DialogUtil;
 import ch.threema.app.utils.DialogUtil;
-import ch.threema.app.utils.IntentDataUtil;
 import ch.threema.app.utils.LogUtil;
 import ch.threema.app.utils.LogUtil;
 import ch.threema.app.utils.QRScannerUtil;
 import ch.threema.app.utils.QRScannerUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.TestUtil;
-import ch.threema.app.webclient.services.QRCodeParser;
-import ch.threema.app.webclient.services.QRCodeParserImpl;
-import ch.threema.client.Base64;
 import ch.threema.localcrypto.MasterKeyLockedException;
 import ch.threema.localcrypto.MasterKeyLockedException;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.ContactModel;
 
 
-import static ch.threema.client.ProtocolDefines.IDENTITY_LEN;
+import static ch.threema.domain.protocol.csp.ProtocolDefines.IDENTITY_LEN;
 
 
 public class AddContactActivity extends ThreemaActivity implements GenericAlertDialog.DialogClickListener, NewContactDialog.NewContactDialogClickListener {
 public class AddContactActivity extends ThreemaActivity implements GenericAlertDialog.DialogClickListener, NewContactDialog.NewContactDialogClickListener {
 	private static final String DIALOG_TAG_ADD_PROGRESS = "ap";
 	private static final String DIALOG_TAG_ADD_PROGRESS = "ap";
@@ -76,7 +71,7 @@ public class AddContactActivity extends ThreemaActivity implements GenericAlertD
 	private static final String DIALOG_TAG_ADD_USER = "au";
 	private static final String DIALOG_TAG_ADD_USER = "au";
 	private static final String DIALOG_TAG_ADD_BY_ID = "abi";
 	private static final String DIALOG_TAG_ADD_BY_ID = "abi";
 	public static final String EXTRA_ADD_BY_ID = "add_by_id";
 	public static final String EXTRA_ADD_BY_ID = "add_by_id";
-	public static final String EXTRA_ADD_BY_QR = "add_by_qr";
+	public static final String EXTRA_QR_RESULT = "qr_result";
 
 
 	private static final int PERMISSION_REQUEST_CAMERA = 1;
 	private static final int PERMISSION_REQUEST_CAMERA = 1;
 
 
@@ -146,9 +141,8 @@ public class AddContactActivity extends ThreemaActivity implements GenericAlertD
 					}
 					}
 				}
 				}
 			}
 			}
-
-			if (intent.getBooleanExtra(EXTRA_ADD_BY_QR, false)) {
-				scanQR();
+			if (intent.getStringExtra(EXTRA_QR_RESULT) != null) {
+				parseQrResult(intent.getStringExtra(EXTRA_QR_RESULT));
 			}
 			}
 
 
 			if (intent.getBooleanExtra(EXTRA_ADD_BY_ID, false)) {
 			if (intent.getBooleanExtra(EXTRA_ADD_BY_ID, false)) {
@@ -157,6 +151,32 @@ public class AddContactActivity extends ThreemaActivity implements GenericAlertD
 		}
 		}
 	}
 	}
 
 
+	private void parseQrResult(String payload) {
+		// first: try to parse as contact result (contact scan)
+		QRCodeService.QRCodeContentResult contactQRCode = this.qrCodeService.getResult(payload);
+
+		if (contactQRCode != null) {
+			addContactByQRResult(contactQRCode);
+			return;
+		}
+
+		// second: try uri scheme
+		String scannedIdentity = null;
+		Uri uri = Uri.parse(payload);
+		if (uri != null) {
+			String scheme = uri.getScheme();
+			if (BuildConfig.uriScheme.equals(scheme) && "add".equals(uri.getAuthority())) {
+				scannedIdentity = uri.getQueryParameter("id");
+			} else if ("https".equals(scheme) && BuildConfig.contactActionUrl.equals(uri.getHost())) {
+				scannedIdentity = uri.getLastPathSegment();
+			}
+
+			if (scannedIdentity != null && scannedIdentity.length() == IDENTITY_LEN) {
+				addContactByIdentity(scannedIdentity);
+			}
+		}
+	}
+
 	@SuppressLint("StaticFieldLeak")
 	@SuppressLint("StaticFieldLeak")
 	private void addContactByIdentity(final String identity) {
 	private void addContactByIdentity(final String identity) {
 		if (lockAppService.isLocked()) {
 		if (lockAppService.isLocked()) {
@@ -216,25 +236,14 @@ public class AddContactActivity extends ThreemaActivity implements GenericAlertD
 		super.onDestroy();
 		super.onDestroy();
 	}
 	}
 
 
-	/**
-	 * start a web client session (payload must be validated before
-	 * the method is called)
-	 *
-	 * fix #ANDR-570
-	 * @param payload a valid payload
-	 */
-	private void startWebClientByQRResult(final byte[] payload) {
-		if (payload != null) {
-			// start web client session screen with payload data and finish my screen
-			Intent webClientIntent = new Intent(this, ch.threema.app.webclient.activities.SessionsActivity.class);
-			IntentDataUtil.append(payload, webClientIntent);
-			this.finish();
-			startActivity(webClientIntent);
-		}
-	}
-
 	@SuppressLint("StaticFieldLeak")
 	@SuppressLint("StaticFieldLeak")
 	private void addContactByQRResult(final QRCodeService.QRCodeContentResult qrResult) {
 	private void addContactByQRResult(final QRCodeService.QRCodeContentResult qrResult) {
+		if (qrResult.getExpirationDate() != null
+			&& qrResult.getExpirationDate().before(new Date())) {
+			GenericAlertDialog.newInstance(R.string.title_adduser, getString(R.string.expired_barcode), R.string.ok, 0).show(getSupportFragmentManager(), "ex");
+			return;
+		}
+
 		ContactModel contactModel = contactService.getByPublicKey(qrResult.getPublicKey());
 		ContactModel contactModel = contactService.getByPublicKey(qrResult.getPublicKey());
 
 
 		if (contactModel != null) {
 		if (contactModel != null) {
@@ -327,7 +336,7 @@ public class AddContactActivity extends ThreemaActivity implements GenericAlertD
 
 
 	private void scanQR() {
 	private void scanQR() {
 		if (ConfigUtils.requestCameraPermissions(this, null, PERMISSION_REQUEST_CAMERA)) {
 		if (ConfigUtils.requestCameraPermissions(this, null, PERMISSION_REQUEST_CAMERA)) {
-			QRScannerUtil.getInstance().initiateScan(this, false, null);
+			QRScannerUtil.getInstance().initiateGeneralThreemaQrScanner(this, getString(R.string.qr_scanner_id_hint));
 		}
 		}
 	}
 	}
 
 
@@ -340,72 +349,6 @@ public class AddContactActivity extends ThreemaActivity implements GenericAlertD
 		dialogFragment.show(getSupportFragmentManager(), DIALOG_TAG_ADD_BY_ID);
 		dialogFragment.show(getSupportFragmentManager(), DIALOG_TAG_ADD_BY_ID);
 	}
 	}
 
 
-	@Override
-	public void onActivityResult(int requestCode, int resultCode, Intent intent) {
-		super.onActivityResult(requestCode, resultCode, intent);
-
-		ConfigUtils.setLocaleOverride(this, serviceManager.getPreferenceService());
-
-		if (resultCode == RESULT_OK) {
-			String payload = QRScannerUtil.getInstance().parseActivityResult(this, requestCode, resultCode, intent);
-
-			if (!TestUtil.empty(payload)) {
-
-				// first: try to parse as content result (contact scan)
-				QRCodeService.QRCodeContentResult contactQRCode = this.qrCodeService.getResult(payload);
-
-				if (contactQRCode != null) {
-					// ok, try to add contact
-					if (contactQRCode.getExpirationDate() != null
-							&& contactQRCode.getExpirationDate().before(new Date())) {
-						GenericAlertDialog.newInstance(R.string.title_adduser, getString(R.string.expired_barcode), R.string.ok, 0).show(getSupportFragmentManager(), "ex");
-					} else {
-						addContactByQRResult(contactQRCode);
-					}
-
-					// return, qr code valid and exit method
-					DialogUtil.dismissDialog(getSupportFragmentManager(), DIALOG_TAG_ADD_BY_ID, true);
-					return;
-				}
-
-				// second: try uri scheme
-				String scannedIdentity = null;
-				Uri uri = Uri.parse(payload);
-				if (uri != null) {
-					String scheme = uri.getScheme();
-					if (BuildConfig.uriScheme.equals(scheme) && "add".equals(uri.getAuthority())) {
-						scannedIdentity = uri.getQueryParameter("id");
-					} else if ("https".equals(scheme) && BuildConfig.contactActionUrl.equals(uri.getHost())) {
-						scannedIdentity = uri.getLastPathSegment();
-					}
-
-					if (scannedIdentity != null && scannedIdentity.length() == IDENTITY_LEN) {
-						addContactByIdentity(scannedIdentity);
-						return;
-					}
-				}
-
-				// third: try to parse as web client qr
-				try {
-					byte[] base64Payload = Base64.decode(payload);
-					if (base64Payload != null) {
-						final QRCodeParser webClientQRCodeParser = new QRCodeParserImpl();
-						webClientQRCodeParser.parse(base64Payload); // throws if QR is not valid
-						// it was a valid web client qr code, exit method
-						startWebClientByQRResult(base64Payload);
-						return;
-					}
-				} catch (IOException | QRCodeParser.InvalidQrCodeException x) {
-					// not a valid base64 or web client payload
-					// ignore and continue
-				}
-			}
-			Toast.makeText(this, R.string.invalid_barcode, Toast.LENGTH_SHORT).show();
-		}
-
-		finish();
-	}
-
 	@Override
 	@Override
 	public void onYes(String tag, Object data) {
 	public void onYes(String tag, Object data) {
 		finish();
 		finish();

+ 45 - 35
app/src/main/java/ch/threema/app/activities/AppLinksActivity.java

@@ -26,19 +26,17 @@ import android.net.Uri;
 import android.os.Bundle;
 import android.os.Bundle;
 import android.widget.Toast;
 import android.widget.Toast;
 
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
+import ch.threema.app.BuildConfig;
 import ch.threema.app.R;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.asynctasks.AddContactAsyncTask;
 import ch.threema.app.asynctasks.AddContactAsyncTask;
+import ch.threema.app.grouplinks.OutgoingGroupRequestActivity;
 import ch.threema.app.services.LockAppService;
 import ch.threema.app.services.LockAppService;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.HiddenChatUtil;
 import ch.threema.app.utils.HiddenChatUtil;
-import ch.threema.client.ProtocolDefines;
+import ch.threema.domain.protocol.csp.ProtocolDefines;
 
 
 public class AppLinksActivity extends ThreemaToolbarActivity {
 public class AppLinksActivity extends ThreemaToolbarActivity {
-	private static final Logger logger = LoggerFactory.getLogger(AppLinksActivity.class);
 
 
 	public void onCreate(Bundle savedInstanceState) {
 	public void onCreate(Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
 		super.onCreate(savedInstanceState);
@@ -81,40 +79,53 @@ public class AppLinksActivity extends ThreemaToolbarActivity {
 	private void handleIntent() {
 	private void handleIntent() {
 		String appLinkAction = getIntent().getAction();
 		String appLinkAction = getIntent().getAction();
 		final Uri appLinkData = getIntent().getData();
 		final Uri appLinkData = getIntent().getData();
-		if (Intent.ACTION_VIEW.equals(appLinkAction) && appLinkData != null){
-			final String threemaId = appLinkData.getLastPathSegment();
-			if (threemaId != null) {
-				if (threemaId.equalsIgnoreCase("compose")) {
-					Intent intent = new Intent(this, RecipientListActivity.class);
-					intent.setAction(appLinkAction);
-					intent.setData(appLinkData);
+		if (Intent.ACTION_VIEW.equals(appLinkAction) && appLinkData.getHost().equals(BuildConfig.contactActionUrl)) {
+			handleContactUrl(appLinkAction, appLinkData);
+		}
+		else if (Intent.ACTION_VIEW.equals(appLinkAction) && appLinkData.getHost().equals(BuildConfig.groupLinkActionUrl)) {
+			handleGroupLinkUrl(appLinkData);
+		}
+		finish();
+	}
+
+	private void handleContactUrl(String appLinkAction, Uri appLinkData) {
+		final String threemaId = appLinkData.getLastPathSegment();
+		if (threemaId != null) {
+			if (threemaId.equalsIgnoreCase("compose")) {
+				Intent intent = new Intent(this, RecipientListActivity.class);
+				intent.setAction(appLinkAction);
+				intent.setData(appLinkData);
+				startActivity(intent);
+			} else if (threemaId.length() == ProtocolDefines.IDENTITY_LEN) {
+				new AddContactAsyncTask(null, null, threemaId, false, () -> {
+					String text = appLinkData.getQueryParameter("text");
+
+					Intent intent = new Intent(AppLinksActivity.this, text != null ?
+						ComposeMessageActivity.class :
+						ContactDetailActivity.class);
+					intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+					intent.putExtra(ThreemaApplication.INTENT_DATA_CONTACT, threemaId);
+					intent.putExtra(ThreemaApplication.INTENT_DATA_EDITFOCUS, Boolean.TRUE);
+
+					if (text != null) {
+						text = text.trim();
+						intent.putExtra(ThreemaApplication.INTENT_DATA_TEXT, text);
+					}
+
 					startActivity(intent);
 					startActivity(intent);
-				} else if (threemaId.length() == ProtocolDefines.IDENTITY_LEN) {
-					new AddContactAsyncTask(null, null, threemaId, false, () -> {
-						String text = appLinkData.getQueryParameter("text");
-
-						Intent intent = new Intent(AppLinksActivity.this, text != null ?
-							ComposeMessageActivity.class :
-							ContactDetailActivity.class);
-						intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-						intent.putExtra(ThreemaApplication.INTENT_DATA_CONTACT, threemaId);
-						intent.putExtra(ThreemaApplication.INTENT_DATA_EDITFOCUS, Boolean.TRUE);
-
-						if (text != null) {
-							text = text.trim();
-							intent.putExtra(ThreemaApplication.INTENT_DATA_TEXT, text);
-						}
-
-						startActivity(intent);
-					}).execute();
-				} else {
-					Toast.makeText(this, R.string.invalid_input, Toast.LENGTH_LONG).show();
-				}
+				}).execute();
 			} else {
 			} else {
 				Toast.makeText(this, R.string.invalid_input, Toast.LENGTH_LONG).show();
 				Toast.makeText(this, R.string.invalid_input, Toast.LENGTH_LONG).show();
 			}
 			}
+		} else {
+			Toast.makeText(this, R.string.invalid_input, Toast.LENGTH_LONG).show();
 		}
 		}
-		finish();
+	}
+
+	private void handleGroupLinkUrl(Uri appLinkData) {
+		Intent intent = new Intent(AppLinksActivity.this, OutgoingGroupRequestActivity.class);
+		intent.putExtra(ThreemaApplication.INTENT_DATA_GROUP_LINK, appLinkData.getEncodedFragment());
+		startActivity(intent);
 	}
 	}
 
 
 	@Override
 	@Override
@@ -125,7 +136,6 @@ public class AppLinksActivity extends ThreemaToolbarActivity {
 
 
 	@Override
 	@Override
 	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
 	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-		logger.debug("onActivityResult");
 		switch (requestCode) {
 		switch (requestCode) {
 			case ThreemaActivity.ACTIVITY_ID_CHECK_LOCK:
 			case ThreemaActivity.ACTIVITY_ID_CHECK_LOCK:
 				if (resultCode == RESULT_OK) {
 				if (resultCode == RESULT_OK) {

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

@@ -98,7 +98,7 @@ import ch.threema.app.utils.ViewUtil;
 import ch.threema.app.voip.services.VoipStateService;
 import ch.threema.app.voip.services.VoipStateService;
 import ch.threema.app.voip.util.VoipUtil;
 import ch.threema.app.voip.util.VoipUtil;
 import ch.threema.base.ThreemaException;
 import ch.threema.base.ThreemaException;
-import ch.threema.base.VerificationLevel;
+import ch.threema.domain.models.VerificationLevel;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.GroupModel;
 import ch.threema.storage.models.GroupModel;
 
 
@@ -883,6 +883,11 @@ public class ContactDetailActivity extends ThreemaToolbarActivity
 		switch (tag) {
 		switch (tag) {
 			case DIALOG_TAG_DELETE_CONTACT:
 			case DIALOG_TAG_DELETE_CONTACT:
 				deleteContact((ContactModel) data);
 				deleteContact((ContactModel) data);
+				try {
+					serviceManager.getShortcutService().deleteShortcut((ContactModel) data);
+				} catch (ThreemaException e) {
+					logger.error("Exception, failed to delete direct share shortcut", e);
+				}
 				break;
 				break;
 			case DIALOG_TAG_EXCLUDE_CONTACT:
 			case DIALOG_TAG_EXCLUDE_CONTACT:
 				removeContactConfirmed(true, (ContactModel) data);
 				removeContactConfirmed(true, (ContactModel) data);

+ 8 - 14
app/src/main/java/ch/threema/app/activities/DirectoryActivity.java

@@ -69,9 +69,9 @@ import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.IntentDataUtil;
 import ch.threema.app.utils.IntentDataUtil;
 import ch.threema.app.utils.LogUtil;
 import ch.threema.app.utils.LogUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.TestUtil;
-import ch.threema.client.work.WorkDirectoryCategory;
-import ch.threema.client.work.WorkDirectoryContact;
-import ch.threema.client.work.WorkOrganization;
+import ch.threema.domain.protocol.api.work.WorkDirectoryCategory;
+import ch.threema.domain.protocol.api.work.WorkDirectoryContact;
+import ch.threema.domain.protocol.api.work.WorkOrganization;
 
 
 public class DirectoryActivity extends ThreemaToolbarActivity implements ThreemaSearchView.OnQueryTextListener, MultiChoiceSelectorDialog.SelectorDialogClickListener {
 public class DirectoryActivity extends ThreemaToolbarActivity implements ThreemaSearchView.OnQueryTextListener, MultiChoiceSelectorDialog.SelectorDialogClickListener {
 	private static final Logger logger = LoggerFactory.getLogger(DirectoryActivity.class);
 	private static final Logger logger = LoggerFactory.getLogger(DirectoryActivity.class);
@@ -90,15 +90,15 @@ public class DirectoryActivity extends ThreemaToolbarActivity implements Threema
 	private ChipGroup chipGroup;
 	private ChipGroup chipGroup;
 
 
 	private List<WorkDirectoryCategory> categoryList = new ArrayList<>();
 	private List<WorkDirectoryCategory> categoryList = new ArrayList<>();
-	private List<WorkDirectoryCategory> checkedCategories = new ArrayList<>();
+	private final List<WorkDirectoryCategory> checkedCategories = new ArrayList<>();
 
 
 	private String queryText;
 	private String queryText;
 
 
 	@ColorInt int categorySpanColor;
 	@ColorInt int categorySpanColor;
 	@ColorInt int categorySpanTextColor;
 	@ColorInt int categorySpanTextColor;
 
 
-	private Handler queryHandler = new Handler();
-	private Runnable queryTask = new Runnable() {
+	private final Handler queryHandler = new Handler();
+	private final Runnable queryTask = new Runnable() {
 		@Override
 		@Override
 		public void run() {
 		public void run() {
 			directoryDataSourceFactory.postLiveData.getValue().setQueryText(queryText);
 			directoryDataSourceFactory.postLiveData.getValue().setQueryText(queryText);
@@ -161,13 +161,7 @@ public class DirectoryActivity extends ThreemaToolbarActivity implements Threema
 
 
 		categoryList = preferenceService.getWorkDirectoryCategories();
 		categoryList = preferenceService.getWorkDirectoryCategories();
 
 
-		if (categoryList.size() > 0) {
-			if (ConfigUtils.getAppTheme(this) == ConfigUtils.THEME_DARK) {
-				ConfigUtils.themeImageView(this, findViewById(R.id.category_selector_button));
-			}
-		} else {
-			findViewById(R.id.category_selector_button).setVisibility(View.GONE);
-		}
+		findViewById(R.id.category_selector_button).setVisibility(categoryList.size() > 0 ? View.VISIBLE : View.GONE);
 
 
 		WorkOrganization workOrganization = preferenceService.getWorkOrganization();
 		WorkOrganization workOrganization = preferenceService.getWorkOrganization();
 		if (workOrganization != null && !TestUtil.empty(workOrganization.getName())) {
 		if (workOrganization != null && !TestUtil.empty(workOrganization.getName())) {
@@ -335,7 +329,7 @@ public class DirectoryActivity extends ThreemaToolbarActivity implements Threema
 
 
 				Chip chip = new Chip(this);
 				Chip chip = new Chip(this);
 				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-					chip.setTextAppearance(R.style.TextAppearance_Chip_Ballot);
+					chip.setTextAppearance(R.style.TextAppearance_Chip_ChatNotice);
 				} else {
 				} else {
 					chip.setTextSize(14);
 					chip.setTextSize(14);
 				}
 				}

+ 135 - 63
app/src/main/java/ch/threema/app/activities/EnterSerialActivity.java

@@ -49,8 +49,10 @@ import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.dialogs.GenericProgressDialog;
 import ch.threema.app.dialogs.GenericProgressDialog;
 import ch.threema.app.exceptions.FileSystemNotPresentException;
 import ch.threema.app.exceptions.FileSystemNotPresentException;
+import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.push.PushService;
 import ch.threema.app.push.PushService;
 import ch.threema.app.services.AppRestrictionService;
 import ch.threema.app.services.AppRestrictionService;
+import ch.threema.app.services.PreferenceService;
 import ch.threema.app.services.license.LicenseService;
 import ch.threema.app.services.license.LicenseService;
 import ch.threema.app.services.license.LicenseServiceUser;
 import ch.threema.app.services.license.LicenseServiceUser;
 import ch.threema.app.services.license.SerialCredentials;
 import ch.threema.app.services.license.SerialCredentials;
@@ -59,6 +61,7 @@ import ch.threema.app.utils.AppRestrictionUtil;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.DialogUtil;
 import ch.threema.app.utils.DialogUtil;
 import ch.threema.app.utils.EditTextUtil;
 import ch.threema.app.utils.EditTextUtil;
+import ch.threema.app.utils.LocaleUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.TestUtil;
 
 
 // this should NOT extend ThreemaToolbarActivity
 // this should NOT extend ThreemaToolbarActivity
@@ -67,14 +70,15 @@ public class EnterSerialActivity extends ThreemaActivity {
 
 
 	private static final String BUNDLE_PASSWORD = "bupw";
 	private static final String BUNDLE_PASSWORD = "bupw";
 	private static final String BUNDLE_LICENSE_KEY = "bulk";
 	private static final String BUNDLE_LICENSE_KEY = "bulk";
+	private static final String BUNDLE_SERVER = "busv";
 	private static final String DIALOG_TAG_CHECKING = "check";
 	private static final String DIALOG_TAG_CHECKING = "check";
 	private TextView stateTextView, privateExplainText = null;
 	private TextView stateTextView, privateExplainText = null;
-	private EditText licenseKeyText, passwordText;
+	private EditText licenseKeyOrUsernameText, passwordText, serverText;
 	private ImageView unlockButton;
 	private ImageView unlockButton;
 	private Button loginButton;
 	private Button loginButton;
 	private LicenseService licenseService;
 	private LicenseService licenseService;
+	private PreferenceService preferenceService;
 
 
-	@SuppressLint("StringFormatInvalid")
 	public void onCreate(Bundle savedInstanceState) {
 	public void onCreate(Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
 		super.onCreate(savedInstanceState);
 
 
@@ -85,8 +89,18 @@ public class EnterSerialActivity extends ThreemaActivity {
 
 
 		setContentView(R.layout.activity_enter_serial);
 		setContentView(R.layout.activity_enter_serial);
 
 
+		ServiceManager serviceManager = ThreemaApplication.getServiceManager();
+
+		if (serviceManager == null) {
+			// hide keyboard to make error message visible on low resolution displays
+			EditTextUtil.hideSoftKeyboard(this.licenseKeyOrUsernameText);
+			Toast.makeText(this, "Service Manager not available", Toast.LENGTH_LONG).show();
+			return;
+		}
+
 		try {
 		try {
-			licenseService = ThreemaApplication.getServiceManager().getLicenseService();
+			licenseService = serviceManager.getLicenseService();
+			preferenceService = serviceManager.getPreferenceService();
 		} catch (NullPointerException|FileSystemNotPresentException e) {
 		} catch (NullPointerException|FileSystemNotPresentException e) {
 			logger.error("Exception", e);
 			logger.error("Exception", e);
 			Toast.makeText(this, "Service Manager not available", Toast.LENGTH_LONG).show();
 			Toast.makeText(this, "Service Manager not available", Toast.LENGTH_LONG).show();
@@ -100,60 +114,88 @@ public class EnterSerialActivity extends ThreemaActivity {
 		}
 		}
 
 
 		stateTextView = findViewById(R.id.unlock_state);
 		stateTextView = findViewById(R.id.unlock_state);
-		licenseKeyText = findViewById(R.id.passphrase);
-		passwordText = findViewById(R.id.password);
-
-		if (!ConfigUtils.isWorkBuild()) {
-			licenseKeyText.addTextChangedListener(new PasswordWatcher());
-			licenseKeyText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
-			licenseKeyText.setFilters(new InputFilter[]{new InputFilter.AllCaps(), new InputFilter.LengthFilter(11)});
-			licenseKeyText.setOnKeyListener(new View.OnKeyListener() {
-				@Override
-				public boolean onKey(View v, int keyCode, KeyEvent event) {
-					if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
-						if (licenseKeyText.getText().length() == 11) {
-							doUnlock();
-						}
-						return true;
-					}
-					return false;
-				}
-			});
-			unlockButton = findViewById(R.id.unlock_button);
-			unlockButton.setOnClickListener(new View.OnClickListener() {
-				@Override
-				public void onClick(View v) {
-					doUnlock();
-				}
-			});
+		licenseKeyOrUsernameText = findViewById(R.id.license_key);
+		passwordText = findViewById(getResources().getIdentifier("password", "id", getPackageName()));
+		serverText = findViewById(getResources().getIdentifier("server", "id", getPackageName()));
 
 
-			this.enableLogin(false);
+		if (!ConfigUtils.isWorkBuild() && !ConfigUtils.isOnPremBuild()) {
+			setupForShopBuild();
 		} else {
 		} else {
-			privateExplainText = findViewById(R.id.private_explain);
-			if (privateExplainText != null) {
-				if (PushService.hmsServicesInstalled(this)) {
-					privateExplainText.setText(Html.fromHtml(String.format(getString(R.string.private_threema_download), getString(R.string.private_download_url_hms))));
-				}
-				else {
-					privateExplainText.setText(Html.fromHtml(String.format(getString(R.string.private_threema_download), getString(R.string.private_download_url))));
+			setupForWorkBuild();
+		}
+
+		handleUrlIntent();
+	}
+
+	private void setupForShopBuild() {
+		licenseKeyOrUsernameText.addTextChangedListener(new PasswordWatcher());
+		licenseKeyOrUsernameText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
+		licenseKeyOrUsernameText.setFilters(new InputFilter[]{new InputFilter.AllCaps(), new InputFilter.LengthFilter(11)});
+		licenseKeyOrUsernameText.setOnKeyListener(new View.OnKeyListener() {
+			@Override
+			public boolean onKey(View v, int keyCode, KeyEvent event) {
+				if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
+					if (licenseKeyOrUsernameText.getText().length() == 11) {
+						doUnlock();
+					}
+					return true;
 				}
 				}
-				privateExplainText.setClickable(true);
-				privateExplainText.setMovementMethod (LinkMovementMethod.getInstance());
+				return false;
 			}
 			}
-			licenseKeyText.addTextChangedListener(new TextChangeWatcher());
-			passwordText.addTextChangedListener(new TextChangeWatcher());
-			loginButton = findViewById(R.id.unlock_button_work);
-			loginButton.setOnClickListener(new View.OnClickListener() {
-				@Override
-				public void onClick(View v) {
-					doUnlock();
-				}
-			});
+		});
+		unlockButton = findViewById(R.id.unlock_button);
+		unlockButton.setOnClickListener(new View.OnClickListener() {
+			@Override
+			public void onClick(View v) {
+				doUnlock();
+			}
+		});
+
+		this.enableLogin(false);
+	}
 
 
-			//always enable login button
-			this.enableLogin(true);
+	@SuppressLint("StringFormatInvalid")
+	private void setupForWorkBuild() {
+		privateExplainText = findViewById(R.id.private_explain);
+
+		if (privateExplainText != null) {
+			String workInfoUrl = String.format(getString(R.string.threema_work_url), LocaleUtil.getAppLanguage());
+
+			if (PushService.hmsServicesInstalled(this)) {
+				privateExplainText.setText(Html.fromHtml(
+					String.format(getString(R.string.private_threema_download),
+						workInfoUrl,
+						getString(R.string.private_download_url)
+						)
+					)
+				);
+			}
+			else {
+				privateExplainText.setText(Html.fromHtml
+					(String.format(getString(R.string.private_threema_download),
+						workInfoUrl,
+						getString(R.string.private_download_url))
+					)
+				);
+			}
+			privateExplainText.setClickable(true);
+			privateExplainText.setMovementMethod(LinkMovementMethod.getInstance());
 		}
 		}
+		licenseKeyOrUsernameText.addTextChangedListener(new TextChangeWatcher());
+		passwordText.addTextChangedListener(new TextChangeWatcher());
+		loginButton = findViewById(getResources().getIdentifier("unlock_button_work", "id", getPackageName()));
+		loginButton.setOnClickListener(new View.OnClickListener() {
+			@Override
+			public void onClick(View v) {
+				doUnlock();
+			}
+		});
+
+		//always enable login button
+		this.enableLogin(true);
+	}
 
 
+	private void handleUrlIntent() {
 		String scheme = null;
 		String scheme = null;
 		Uri data = null;
 		Uri data = null;
 		Intent intent = getIntent();
 		Intent intent = getIntent();
@@ -183,8 +225,10 @@ public class EnterSerialActivity extends ThreemaActivity {
 			if (ConfigUtils.isWorkRestricted()) {
 			if (ConfigUtils.isWorkRestricted()) {
 				String username = AppRestrictionUtil.getStringRestriction(getString(R.string.restriction__license_username));
 				String username = AppRestrictionUtil.getStringRestriction(getString(R.string.restriction__license_username));
 				String password = AppRestrictionUtil.getStringRestriction(getString(R.string.restriction__license_password));
 				String password = AppRestrictionUtil.getStringRestriction(getString(R.string.restriction__license_password));
+				String server = AppRestrictionUtil.getStringRestriction(getString(R.string.restriction__onprem_server));
+
 				if (!TestUtil.empty(username) && !TestUtil.empty(password)) {
 				if (!TestUtil.empty(username) && !TestUtil.empty(password)) {
-					check(new UserCredentials(username, password));
+					check(new UserCredentials(username, password), server);
 				}
 				}
 			}
 			}
 		} else {
 		} else {
@@ -197,7 +241,7 @@ public class EnterSerialActivity extends ThreemaActivity {
 	}
 	}
 
 
 	private void enableLogin(boolean enable) {
 	private void enableLogin(boolean enable) {
-		if (!ConfigUtils.isWorkBuild()) {
+		if (!ConfigUtils.isWorkBuild() && !ConfigUtils.isOnPremBuild()) {
 			if (this.unlockButton != null) {
 			if (this.unlockButton != null) {
 				unlockButton.setClickable(enable);
 				unlockButton.setClickable(enable);
 				unlockButton.setEnabled(enable);
 				unlockButton.setEnabled(enable);
@@ -217,14 +261,15 @@ public class EnterSerialActivity extends ThreemaActivity {
 			if (licenseService instanceof LicenseServiceUser) {
 			if (licenseService instanceof LicenseServiceUser) {
 				final String username = data.getQueryParameter("username");
 				final String username = data.getQueryParameter("username");
 				final String password = data.getQueryParameter("password");
 				final String password = data.getQueryParameter("password");
+				final String server = data.getQueryParameter("server");
 				if (!TestUtil.empty(username) && !TestUtil.empty(password)) {
 				if (!TestUtil.empty(username) && !TestUtil.empty(password)) {
-					check(new UserCredentials(username, password));
+					check(new UserCredentials(username, password), server);
 					return;
 					return;
 				}
 				}
 			} else {
 			} else {
 				final String key = data.getQueryParameter("key");
 				final String key = data.getQueryParameter("key");
 				if (!TestUtil.empty(key)) {
 				if (!TestUtil.empty(key)) {
-					check(new SerialCredentials(key));
+					check(new SerialCredentials(key), null);
 					return;
 					return;
 				}
 				}
 			}
 			}
@@ -234,19 +279,26 @@ public class EnterSerialActivity extends ThreemaActivity {
 
 
 	private void doUnlock() {
 	private void doUnlock() {
 		// hide keyboard to make error message visible on low resolution displays
 		// hide keyboard to make error message visible on low resolution displays
-		EditTextUtil.hideSoftKeyboard(this.licenseKeyText);
+		EditTextUtil.hideSoftKeyboard(this.licenseKeyOrUsernameText);
 
 
 		this.enableLogin(false);
 		this.enableLogin(false);
 
 
-		if (ConfigUtils.isWorkBuild()) {
-			if (!TestUtil.empty(this.licenseKeyText.getText().toString()) && !TestUtil.empty(this.passwordText.getText().toString())) {
-				this.check(new UserCredentials(this.licenseKeyText.getText().toString(), this.passwordText.getText().toString()));
+		if (ConfigUtils.isOnPremBuild()) {
+			if (!TestUtil.empty(this.licenseKeyOrUsernameText.getText().toString()) && !TestUtil.empty(this.passwordText.getText().toString()) && !TestUtil.empty(this.serverText.getText().toString())) {
+				this.check(new UserCredentials(this.licenseKeyOrUsernameText.getText().toString(), this.passwordText.getText().toString()), this.serverText.getText().toString());
+			} else {
+				this.enableLogin(true);
+				this.stateTextView.setText(getString(R.string.invalid_input));
+			}
+		} else if (ConfigUtils.isWorkBuild()) {
+			if (!TestUtil.empty(this.licenseKeyOrUsernameText.getText().toString()) && !TestUtil.empty(this.passwordText.getText().toString())) {
+				this.check(new UserCredentials(this.licenseKeyOrUsernameText.getText().toString(), this.passwordText.getText().toString()), null);
 			} else {
 			} else {
 				this.enableLogin(true);
 				this.enableLogin(true);
 				this.stateTextView.setText(getString(R.string.invalid_input));
 				this.stateTextView.setText(getString(R.string.invalid_input));
 			}
 			}
 		} else {
 		} else {
-			this.check(new SerialCredentials(this.licenseKeyText.getText().toString()));
+			this.check(new SerialCredentials(this.licenseKeyOrUsernameText.getText().toString()), null);
 		}
 		}
 	}
 	}
 
 
@@ -298,17 +350,37 @@ public class EnterSerialActivity extends ThreemaActivity {
 	protected void onSaveInstanceState(Bundle outState) {
 	protected void onSaveInstanceState(Bundle outState) {
 		super.onSaveInstanceState(outState);
 		super.onSaveInstanceState(outState);
 
 
-		if (!TestUtil.empty(licenseKeyText.getText())) {
-			outState.putString(BUNDLE_LICENSE_KEY, licenseKeyText.getText().toString());
+		if (!TestUtil.empty(licenseKeyOrUsernameText.getText())) {
+			outState.putString(BUNDLE_LICENSE_KEY, licenseKeyOrUsernameText.getText().toString());
 		}
 		}
 
 
-		if (!TestUtil.empty(passwordText.getText())) {
+		if (passwordText != null && !TestUtil.empty(passwordText.getText())) {
 			outState.putString(BUNDLE_PASSWORD, passwordText.getText().toString());
 			outState.putString(BUNDLE_PASSWORD, passwordText.getText().toString());
 		}
 		}
+
+		if (serverText != null && !TestUtil.empty(serverText.getText())) {
+			outState.putString(BUNDLE_SERVER, serverText.getText().toString());
+		}
 	}
 	}
 
 
 	@SuppressLint("StaticFieldLeak")
 	@SuppressLint("StaticFieldLeak")
-	private void check(final LicenseService.Credentials credentials) {
+	private void check(final LicenseService.Credentials credentials, String onPremServer) {
+		if (ConfigUtils.isOnPremBuild()) {
+			if (onPremServer != null) {
+				if (!onPremServer.startsWith("https://")) {
+					onPremServer = "https://" + onPremServer;
+				}
+
+				if (!onPremServer.endsWith(".oppf")) {
+					// Automatically expand hostnames to default provisioning URL
+					onPremServer += "/prov/config.oppf";
+				}
+			}
+			preferenceService.setOnPremServer(onPremServer);
+			preferenceService.setLicenseUsername(((UserCredentials)credentials).username);
+			preferenceService.setLicensePassword(((UserCredentials)credentials).password);
+		}
+
 		new AsyncTask<Void, Void, String>() {
 		new AsyncTask<Void, Void, String>() {
 			@Override
 			@Override
 			protected void onPreExecute() {
 			protected void onPreExecute() {

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

@@ -42,7 +42,7 @@ import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.DialogUtil;
 import ch.threema.app.utils.DialogUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.base.ThreemaException;
 import ch.threema.base.ThreemaException;
-import ch.threema.client.IdentityBackupGenerator;
+import ch.threema.domain.identitybackup.IdentityBackupGenerator;
 
 
 public class ExportIDActivity extends AppCompatActivity implements PasswordEntryDialog.PasswordEntryDialogClickListener {
 public class ExportIDActivity extends AppCompatActivity implements PasswordEntryDialog.PasswordEntryDialogClickListener {
 	private static final Logger logger = LoggerFactory.getLogger(AppCompatActivity.class);
 	private static final Logger logger = LoggerFactory.getLogger(AppCompatActivity.class);
@@ -79,7 +79,7 @@ public class ExportIDActivity extends AppCompatActivity implements PasswordEntry
 				ThreemaApplication.MIN_PW_LENGTH_BACKUP,
 				ThreemaApplication.MIN_PW_LENGTH_BACKUP,
 				ThreemaApplication.MAX_PW_LENGTH_BACKUP,
 				ThreemaApplication.MAX_PW_LENGTH_BACKUP,
 				R.string.backup_password_again_summary,
 				R.string.backup_password_again_summary,
-				0, 0);
+				0, 0, PasswordEntryDialog.ForgotHintType.NONE);
 		dialogFragment.show(getSupportFragmentManager(), DIALOG_TAG_SET_ID_BACKUP_PW);
 		dialogFragment.show(getSupportFragmentManager(), DIALOG_TAG_SET_ID_BACKUP_PW);
 	}
 	}
 
 

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

@@ -26,7 +26,6 @@ import android.content.Intent;
 import android.content.res.Configuration;
 import android.content.res.Configuration;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.BitmapDrawable;
-import android.os.Build;
 import android.os.Bundle;
 import android.os.Bundle;
 import android.print.PrintAttributes;
 import android.print.PrintAttributes;
 import android.print.PrintDocumentAdapter;
 import android.print.PrintDocumentAdapter;
@@ -176,11 +175,7 @@ public class ExportIDResultActivity extends ThreemaToolbarActivity implements Ge
 				.getSystemService(Context.PRINT_SERVICE);
 				.getSystemService(Context.PRINT_SERVICE);
 
 
 		PrintDocumentAdapter printAdapter;
 		PrintDocumentAdapter printAdapter;
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-			printAdapter = webView.createPrintDocumentAdapter("Threema_ID_" + identity);
-		} else {
-			printAdapter = webView.createPrintDocumentAdapter();
-		}
+		printAdapter = webView.createPrintDocumentAdapter("Threema_ID_" + identity);
 		String jobName = getString(R.string.app_name) + " " + getString(R.string.backup_id_title);
 		String jobName = getString(R.string.app_name) + " " + getString(R.string.backup_id_title);
 
 
 		printManager.print(jobName, printAdapter,
 		printManager.print(jobName, printAdapter,

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

@@ -30,6 +30,7 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.List;
 
 
 import androidx.annotation.NonNull;
 import androidx.annotation.NonNull;
+import ch.threema.app.BuildConfig;
 import ch.threema.app.R;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.dialogs.GenericAlertDialog;
 import ch.threema.app.dialogs.GenericAlertDialog;
@@ -120,8 +121,8 @@ public class GroupAddActivity extends MemberChooseActivity implements GenericAle
 		final int previousContacts = this.appendMembers ? excludedIdentities.size() : 1; // user counts as one contact
 		final int previousContacts = this.appendMembers ? excludedIdentities.size() : 1; // user counts as one contact
 
 
 		if (selectedContacts.size() >= ThreemaApplication.MIN_GROUP_MEMBERS_COUNT) {
 		if (selectedContacts.size() >= ThreemaApplication.MIN_GROUP_MEMBERS_COUNT) {
-			if ((previousContacts + selectedContacts.size()) > getResources().getInteger(R.integer.max_group_size)) {
-				Toast.makeText(this, String.format(getString(R.string.group_select_max), getResources().getInteger(R.integer.max_group_size) - previousContacts), Toast.LENGTH_LONG).show();
+			if ((previousContacts + selectedContacts.size()) > BuildConfig.MAX_GROUP_SIZE) {
+				Toast.makeText(this, String.format(getString(R.string.group_select_max), BuildConfig.MAX_GROUP_SIZE - previousContacts), Toast.LENGTH_LONG).show();
 			} else {
 			} else {
 				createOrUpdateGroup(selectedContacts);
 				createOrUpdateGroup(selectedContacts);
 			}
 			}

+ 155 - 109
app/src/main/java/ch/threema/app/activities/GroupDetailActivity.java

@@ -62,6 +62,7 @@ import androidx.lifecycle.ViewModelProvider;
 import androidx.palette.graphics.Palette;
 import androidx.palette.graphics.Palette;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.recyclerview.widget.RecyclerView;
+import ch.threema.app.BuildConfig;
 import ch.threema.app.R;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.adapters.GroupDetailAdapter;
 import ch.threema.app.adapters.GroupDetailAdapter;
@@ -71,20 +72,24 @@ import ch.threema.app.asynctasks.LeaveGroupAsyncTask;
 import ch.threema.app.dialogs.GenericAlertDialog;
 import ch.threema.app.dialogs.GenericAlertDialog;
 import ch.threema.app.dialogs.GenericProgressDialog;
 import ch.threema.app.dialogs.GenericProgressDialog;
 import ch.threema.app.dialogs.SelectorDialog;
 import ch.threema.app.dialogs.SelectorDialog;
+import ch.threema.app.dialogs.ShowOnceDialog;
 import ch.threema.app.dialogs.SimpleStringAlertDialog;
 import ch.threema.app.dialogs.SimpleStringAlertDialog;
 import ch.threema.app.dialogs.TextEntryDialog;
 import ch.threema.app.dialogs.TextEntryDialog;
 import ch.threema.app.emojis.EmojiEditText;
 import ch.threema.app.emojis.EmojiEditText;
 import ch.threema.app.exceptions.FileSystemNotPresentException;
 import ch.threema.app.exceptions.FileSystemNotPresentException;
+import ch.threema.app.grouplinks.GroupLinkOverviewActivity;
 import ch.threema.app.listeners.ContactListener;
 import ch.threema.app.listeners.ContactListener;
 import ch.threema.app.listeners.ContactSettingsListener;
 import ch.threema.app.listeners.ContactSettingsListener;
 import ch.threema.app.listeners.GroupListener;
 import ch.threema.app.listeners.GroupListener;
 import ch.threema.app.managers.ListenerManager;
 import ch.threema.app.managers.ListenerManager;
 import ch.threema.app.services.DeviceService;
 import ch.threema.app.services.DeviceService;
 import ch.threema.app.services.IdListService;
 import ch.threema.app.services.IdListService;
+import ch.threema.app.services.group.GroupInviteService;
 import ch.threema.app.services.license.LicenseService;
 import ch.threema.app.services.license.LicenseService;
 import ch.threema.app.ui.AvatarEditView;
 import ch.threema.app.ui.AvatarEditView;
 import ch.threema.app.ui.GroupDetailViewModel;
 import ch.threema.app.ui.GroupDetailViewModel;
 import ch.threema.app.ui.ResumePauseHandler;
 import ch.threema.app.ui.ResumePauseHandler;
+import ch.threema.app.ui.SelectorDialogItem;
 import ch.threema.app.utils.AppRestrictionUtil;
 import ch.threema.app.utils.AppRestrictionUtil;
 import ch.threema.app.utils.BitmapUtil;
 import ch.threema.app.utils.BitmapUtil;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.ConfigUtils;
@@ -96,14 +101,20 @@ import ch.threema.app.utils.NameUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.voip.util.VoipUtil;
 import ch.threema.app.voip.util.VoipUtil;
+import ch.threema.base.ThreemaException;
+import ch.threema.localcrypto.MasterKeyLockedException;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.GroupModel;
 import ch.threema.storage.models.GroupModel;
 
 
 import static ch.threema.app.dialogs.ContactEditDialog.CONTACT_AVATAR_HEIGHT_PX;
 import static ch.threema.app.dialogs.ContactEditDialog.CONTACT_AVATAR_HEIGHT_PX;
 
 
-public class GroupDetailActivity extends GroupEditActivity implements SelectorDialog.SelectorDialogClickListener, GenericAlertDialog.DialogClickListener, TextEntryDialog.TextEntryDialogClickListener {
+public class GroupDetailActivity extends GroupEditActivity implements SelectorDialog.SelectorDialogClickListener,
+	GenericAlertDialog.DialogClickListener,
+	TextEntryDialog.TextEntryDialogClickListener,
+	GroupDetailAdapter.OnGroupDetailsClickListener
+	{
 	private static final Logger logger = LoggerFactory.getLogger(GroupDetailActivity.class);
 	private static final Logger logger = LoggerFactory.getLogger(GroupDetailActivity.class);
-
+	// static values
 	private final int MODE_EDIT = 1;
 	private final int MODE_EDIT = 1;
 	private final int MODE_READONLY = 2;
 	private final int MODE_READONLY = 2;
 
 
@@ -116,6 +127,7 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 	private static final String DIALOG_TAG_CLONE_GROUP = "cg";
 	private static final String DIALOG_TAG_CLONE_GROUP = "cg";
 	private static final String DIALOG_TAG_CLONE_GROUP_CONFIRM = "cgc";
 	private static final String DIALOG_TAG_CLONE_GROUP_CONFIRM = "cgc";
 	private static final String DIALOG_TAG_CLONING_GROUP = "cgi";
 	private static final String DIALOG_TAG_CLONING_GROUP = "cgi";
+	public static final String DIALOG_SHOW_ONCE_RESET_LINK_INFO = "resetGroupLink";
 	private static final String RUN_ON_ACTIVE_RELOAD = "reload";
 	private static final String RUN_ON_ACTIVE_RELOAD = "reload";
 
 
 	private static final int SELECTOR_OPTION_CONTACT_DETAIL = 0;
 	private static final int SELECTOR_OPTION_CONTACT_DETAIL = 0;
@@ -123,24 +135,27 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 	private static final int SELECTOR_OPTION_CALL = 2;
 	private static final int SELECTOR_OPTION_CALL = 2;
 	private static final int SELECTOR_OPTION_REMOVE = 3;
 	private static final int SELECTOR_OPTION_REMOVE = 3;
 
 
-	private int operationMode;
-	private int groupId;
-	private EmojiEditText groupNameEditText;
-	private boolean hasChanges = false;
-
-	private String myIdentity;
+	// services
+	private LicenseService licenseService;
+	private GroupInviteService groupInviteService;
+	private DeviceService deviceService;
+	private IdListService blackListIdentityService;
 
 
 	private GroupModel groupModel;
 	private GroupModel groupModel;
+	private GroupDetailViewModel groupDetailViewModel;
 	private GroupDetailAdapter groupDetailAdapter;
 	private GroupDetailAdapter groupDetailAdapter;
+
+	private EmojiEditText groupNameEditText;
 	private CollapsingToolbarLayout collapsingToolbar;
 	private CollapsingToolbarLayout collapsingToolbar;
 	private ResumePauseHandler resumePauseHandler;
 	private ResumePauseHandler resumePauseHandler;
-	private DeviceService deviceService;
-	private IdListService blackListIdentityService;
-	private LicenseService licenseService;
 	private AvatarEditView avatarEditView;
 	private AvatarEditView avatarEditView;
-	private GroupDetailViewModel groupDetailViewModel;
 	private ExtendedFloatingActionButton floatingActionButton;
 	private ExtendedFloatingActionButton floatingActionButton;
 
 
+	private String myIdentity;
+	private int operationMode;
+	private int groupId;
+	private boolean hasChanges = false;
+
 	private final ResumePauseHandler.RunIfActive runIfActiveUpdate = new ResumePauseHandler.RunIfActive() {
 	private final ResumePauseHandler.RunIfActive runIfActiveUpdate = new ResumePauseHandler.RunIfActive() {
 		@Override
 		@Override
 		public void runOnUiThread() {
 		public void runOnUiThread() {
@@ -298,8 +313,9 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 			this.deviceService = serviceManager.getDeviceService();
 			this.deviceService = serviceManager.getDeviceService();
 			this.blackListIdentityService = serviceManager.getBlackListService();
 			this.blackListIdentityService = serviceManager.getBlackListService();
 			this.licenseService = serviceManager.getLicenseService();
 			this.licenseService = serviceManager.getLicenseService();
-		} catch (FileSystemNotPresentException e) {
-			logger.error("Exception", e);
+			this.groupInviteService = serviceManager.getGroupInviteService();
+		} catch (FileSystemNotPresentException | MasterKeyLockedException e) {
+			logger.error("Exception, could not get required services", e);
 			finishUp();
 			finishUp();
 			return;
 			return;
 		}
 		}
@@ -336,16 +352,13 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 		((AppBarLayout) findViewById(R.id.appbar)).addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
 		((AppBarLayout) findViewById(R.id.appbar)).addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
 			@Override
 			@Override
 			public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
 			public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
-				logger.debug("Vertical offset: " + verticalOffset);
 				if (verticalOffset == 0) {
 				if (verticalOffset == 0) {
 					if (!floatingActionButton.isExtended()) {
 					if (!floatingActionButton.isExtended()) {
 						floatingActionButton.extend();
 						floatingActionButton.extend();
 					}
 					}
 				}
 				}
-				else {
-					if (floatingActionButton.isExtended()) {
-						floatingActionButton.shrink();
-					}
+				else if (floatingActionButton.isExtended()) {
+					floatingActionButton.shrink();
 				}
 				}
 			}
 			}
 		});
 		});
@@ -355,22 +368,12 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 
 
 		if (this.groupService.isGroupOwner(this.groupModel)) {
 		if (this.groupService.isGroupOwner(this.groupModel)) {
 			operationMode = MODE_EDIT;
 			operationMode = MODE_EDIT;
-			doneButton.setOnClickListener(new View.OnClickListener() {
-				@Override
-				public void onClick(View v) {
-					saveGroupSettings();
-				}
-			});
-			floatingActionButton.setOnClickListener(new View.OnClickListener() {
-				@Override
-				public void onClick(View v) {
-					if (groupService != null && groupService.isGroupOwner(groupModel)) {
-						Intent intent = new Intent(GroupDetailActivity.this, GroupAddActivity.class);
-						IntentDataUtil.append(groupModel, intent);
-						IntentDataUtil.append(groupDetailViewModel.getGroupContacts(), intent);
-						startActivityForResult(intent, ThreemaActivity.ACTIVITY_ID_GROUP_ADD);
-					}
-				}
+			doneButton.setOnClickListener(v -> saveGroupSettings());
+			floatingActionButton.setOnClickListener(v -> {
+				Intent intent = new Intent(GroupDetailActivity.this, GroupAddActivity.class);
+				IntentDataUtil.append(groupModel, intent);
+				IntentDataUtil.append(groupDetailViewModel.getGroupContacts(), intent);
+				startActivityForResult(intent, ThreemaActivity.ACTIVITY_ID_GROUP_ADD);
 			});
 			});
 			groupNameEditText.setMaxByteSize(GroupModel.GROUP_NAME_MAX_LENGTH_BYTES);
 			groupNameEditText.setMaxByteSize(GroupModel.GROUP_NAME_MAX_LENGTH_BYTES);
 		} else {
 		} else {
@@ -458,51 +461,7 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 				updateFloatingActionButton();
 				updateFloatingActionButton();
 			}
 			}
 		});
 		});
-		this.groupDetailAdapter.setOnClickListener(new GroupDetailAdapter.OnClickListener() {
-			@Override
-			public void onItemClick(View v, ContactModel contactModel) {
-				if (contactModel != null) {
-					String identity = contactModel.getIdentity();
-					String shortName = NameUtil.getShortName(contactModel);
-
-					ArrayList<String> items = new ArrayList<>();
-					ArrayList<Integer> optionsMap = new ArrayList<>();
-
-					items.add(getString(R.string.show_contact));
-					optionsMap.add(SELECTOR_OPTION_CONTACT_DETAIL);
-
-					if (!TestUtil.compare(myIdentity, identity)) {
-						items.add(String.format(getString(R.string.chat_with), shortName));
-						optionsMap.add(SELECTOR_OPTION_CHAT);
-
-						if (ContactUtil.canReceiveVoipMessages(contactModel, blackListIdentityService)
-								&& ConfigUtils.isCallsEnabled(GroupDetailActivity.this, preferenceService, licenseService)
-						) {
-							items.add(String.format(getString(R.string.call_with), shortName));
-							optionsMap.add(SELECTOR_OPTION_CALL);
-						}
-
-						if (operationMode == MODE_EDIT) {
-							if (groupModel != null && !TestUtil.compare(groupModel.getCreatorIdentity(), identity)) {
-								items.add(String.format(getString(R.string.kick_user_from_group), shortName));
-								optionsMap.add(SELECTOR_OPTION_REMOVE);
-							}
-						}
-						SelectorDialog selectorDialog = SelectorDialog.newInstance(null, items, null);
-						SelectorInfo selectorInfo = new SelectorInfo();
-						selectorInfo.contactModel = contactModel;
-						selectorInfo.view = v;
-						selectorInfo.optionsMap = optionsMap;
-						selectorDialog.setData(selectorInfo);
-						try {
-							selectorDialog.show(getSupportFragmentManager(), DIALOG_TAG_CHOOSE_ACTION);
-						} catch (IllegalStateException e) {
-							logger.error("Exception", e);
-						}
-					}
-				}
-			}
-		});
+		this.groupDetailAdapter.setOnClickListener(this);
 	}
 	}
 
 
 	@Override
 	@Override
@@ -565,8 +524,9 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 		MenuItem groupSyncMenu = menu.findItem(R.id.menu_resync);
 		MenuItem groupSyncMenu = menu.findItem(R.id.menu_resync);
 		MenuItem leaveGroupMenu = menu.findItem(R.id.menu_leave_group);
 		MenuItem leaveGroupMenu = menu.findItem(R.id.menu_leave_group);
 		MenuItem deleteGroupMenu = menu.findItem(R.id.menu_delete_group);
 		MenuItem deleteGroupMenu = menu.findItem(R.id.menu_delete_group);
-		MenuItem mediaGalleryMenu = menu.findItem(R.id.menu_gallery);
 		MenuItem cloneMenu = menu.findItem(R.id.menu_clone_group);
 		MenuItem cloneMenu = menu.findItem(R.id.menu_clone_group);
+		MenuItem mediaGalleryMenu = menu.findItem(R.id.menu_gallery);
+		MenuItem groupLinkMenu = menu.findItem(R.id.menu_group_links_manage);
 
 
 		if (AppRestrictionUtil.isCreateGroupDisabled(this)) {
 		if (AppRestrictionUtil.isCreateGroupDisabled(this)) {
 			cloneMenu.setVisible(false);
 			cloneMenu.setVisible(false);
@@ -575,23 +535,22 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 		if (groupModel != null) {
 		if (groupModel != null) {
 			leaveGroupMenu.setVisible(true);
 			leaveGroupMenu.setVisible(true);
 			deleteGroupMenu.setVisible(true);
 			deleteGroupMenu.setVisible(true);
-
 			if (groupService.isGroupOwner(this.groupModel)) {
 			if (groupService.isGroupOwner(this.groupModel)) {
 				// MODE_EDIT
 				// MODE_EDIT
 				groupSyncMenu.setVisible(true);
 				groupSyncMenu.setVisible(true);
+				if (ConfigUtils.supportsGroupLinks()) {
+					groupLinkMenu.setVisible(true);
+				}
 			}
 			}
-		}
 
 
-		if (groupModel != null && !hiddenChatsListService.has(groupService.getUniqueIdString(this.groupModel))) {
-			mediaGalleryMenu.setVisible(true);
-		} else {
-			mediaGalleryMenu.setVisible(false);
+			mediaGalleryMenu.setVisible(!hiddenChatsListService.has(groupService.getUniqueIdString(this.groupModel)));
 		}
 		}
 
 
 		if (operationMode != MODE_READONLY) {
 		if (operationMode != MODE_READONLY) {
 			menu.findItem(R.id.action_send_message).setVisible(false);
 			menu.findItem(R.id.action_send_message).setVisible(false);
 		}
 		}
 
 
+
 		return super.onPrepareOptionsMenu(menu);
 		return super.onPrepareOptionsMenu(menu);
 	}
 	}
 
 
@@ -615,7 +574,14 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 		if (itemId == android.R.id.home) {
 		if (itemId == android.R.id.home) {
 			finishUp();
 			finishUp();
 			return true;
 			return true;
-		} else if (itemId == R.id.action_send_message) {
+		}
+		else if (itemId == R.id.menu_group_links_manage) {
+			Intent groupLinkOverviewIntent = new Intent(this, GroupLinkOverviewActivity.class);
+			groupLinkOverviewIntent.putExtra(ThreemaApplication.INTENT_DATA_GROUP, groupId);
+			startActivityForResult(groupLinkOverviewIntent, ThreemaActivity.ACTIVITY_ID_MANAGE_GROUP_LINKS);
+
+		}
+		else if (itemId == R.id.action_send_message) {
 			if (groupModel != null) {
 			if (groupModel != null) {
 				Intent intent = new Intent(this, ComposeMessageActivity.class);
 				Intent intent = new Intent(this, ComposeMessageActivity.class);
 				intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
 				intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
@@ -624,9 +590,11 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 				startActivity(intent);
 				startActivity(intent);
 				finish();
 				finish();
 			}
 			}
-		} else if (itemId == R.id.menu_resync) {
+		}
+		else if (itemId == R.id.menu_resync) {
 			this.syncGroup();
 			this.syncGroup();
-		} else if (itemId == R.id.menu_leave_group) {
+		}
+		else if (itemId == R.id.menu_leave_group) {
 			int leaveMessageRes = operationMode == MODE_READONLY ? R.string.really_leave_group_message : R.string.really_leave_group_admin_message;
 			int leaveMessageRes = operationMode == MODE_READONLY ? R.string.really_leave_group_message : R.string.really_leave_group_admin_message;
 
 
 			GenericAlertDialog.newInstance(
 			GenericAlertDialog.newInstance(
@@ -635,20 +603,16 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 				R.string.ok,
 				R.string.ok,
 				R.string.cancel)
 				R.string.cancel)
 				.show(getSupportFragmentManager(), DIALOG_TAG_LEAVE_GROUP);
 				.show(getSupportFragmentManager(), DIALOG_TAG_LEAVE_GROUP);
-		} else if (itemId == R.id.menu_delete_group) {
+		}
+		else if (itemId == R.id.menu_delete_group) {
 			GenericAlertDialog.newInstance(
 			GenericAlertDialog.newInstance(
 				R.string.action_delete_group,
 				R.string.action_delete_group,
 				groupService.isGroupOwner(groupModel) ? R.string.delete_my_group_message : R.string.delete_group_message,
 				groupService.isGroupOwner(groupModel) ? R.string.delete_my_group_message : R.string.delete_group_message,
 				R.string.ok,
 				R.string.ok,
 				R.string.cancel)
 				R.string.cancel)
 				.show(getSupportFragmentManager(), DIALOG_TAG_DELETE_GROUP);
 				.show(getSupportFragmentManager(), DIALOG_TAG_DELETE_GROUP);
-		} else if (itemId == R.id.menu_gallery) {
-			if (groupId > 0 && !hiddenChatsListService.has(groupService.getUniqueIdString(this.groupModel))) {
-				Intent mediaGalleryIntent = new Intent(this, MediaGalleryActivity.class);
-				mediaGalleryIntent.putExtra(ThreemaApplication.INTENT_DATA_GROUP, groupId);
-				startActivity(mediaGalleryIntent);
-			}
-		} else if (itemId == R.id.menu_clone_group) {
+		}
+		else if (itemId == R.id.menu_clone_group) {
 			GenericAlertDialog.newInstance(
 			GenericAlertDialog.newInstance(
 				R.string.action_clone_group,
 				R.string.action_clone_group,
 				R.string.clone_group_message,
 				R.string.clone_group_message,
@@ -656,6 +620,13 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 				R.string.no)
 				R.string.no)
 				.show(getSupportFragmentManager(), DIALOG_TAG_CLONE_GROUP_CONFIRM);
 				.show(getSupportFragmentManager(), DIALOG_TAG_CLONE_GROUP_CONFIRM);
 		}
 		}
+		else if (itemId == R.id.menu_gallery) {
+			if (groupId > 0 && !hiddenChatsListService.has(groupService.getUniqueIdString(this.groupModel))) {
+				Intent mediaGalleryIntent = new Intent(this, MediaGalleryActivity.class);
+				mediaGalleryIntent.putExtra(ThreemaApplication.INTENT_DATA_GROUP, groupId);
+				startActivity(mediaGalleryIntent);
+			}
+		}
 		return super.onOptionsItemSelected(item);
 		return super.onOptionsItemSelected(item);
 	}
 	}
 
 
@@ -691,7 +662,7 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 							groupService.getGroupIdentities(groupModel),
 							groupService.getGroupIdentities(groupModel),
 							avatar);
 							avatar);
 				} catch (Exception e) {
 				} catch (Exception e) {
-					logger.error("Exception", e);
+					logger.error("Exception, cloning group failed", e);
 					return null;
 					return null;
 				}
 				}
 
 
@@ -807,7 +778,12 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 				groupDetailViewModel.addGroupContacts(IntentDataUtil.getContactIdentities(data));
 				groupDetailViewModel.addGroupContacts(IntentDataUtil.getContactIdentities(data));
 				sortGroupMembers();
 				sortGroupMembers();
 				this.hasChanges = true;
 				this.hasChanges = true;
-			} else {
+			}
+			else if (this.groupService.isGroupOwner(this.groupModel) && requestCode == ThreemaActivity.ACTIVITY_ID_MANAGE_GROUP_LINKS) {
+				// make sure we reset the default link switch if the default link was deleted
+				groupDetailAdapter.notifyDataSetChanged();
+			}
+			else {
 				if (this.avatarEditView != null) {
 				if (this.avatarEditView != null) {
 					this.avatarEditView.onActivityResult(requestCode, resultCode, data);
 					this.avatarEditView.onActivityResult(requestCode, resultCode, data);
 				}
 				}
@@ -889,6 +865,11 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 				break;
 				break;
 			case DIALOG_TAG_DELETE_GROUP:
 			case DIALOG_TAG_DELETE_GROUP:
 				deleteGroupAndQuit();
 				deleteGroupAndQuit();
+				try {
+					serviceManager.getShortcutService().deleteShortcut((groupModel));
+				} catch (ThreemaException e) {
+					logger.debug("Exception, failed to delete direct group shortcut", e);
+				}
 				break;
 				break;
 			case DIALOG_TAG_QUIT:
 			case DIALOG_TAG_QUIT:
 				saveGroupSettings();
 				saveGroupSettings();
@@ -936,15 +917,18 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 	}
 	}
 
 
 	private void updateFloatingActionButton() {
 	private void updateFloatingActionButton() {
-		if (this.floatingActionButton != null &&
-			this.groupService != null &&
-			this.groupDetailAdapter != null) {
-			if (this.groupService.isGroupOwner(this.groupModel)) {
-				if (this.groupDetailAdapter.getItemCount() > getResources().getInteger(R.integer.max_group_size)) {
-					this.floatingActionButton.hide();
-				} else {
-					this.floatingActionButton.show();
-				}
+		if (this.floatingActionButton == null ||
+			this.groupService == null ||
+			this.groupDetailAdapter == null) {
+			logger.error("Exception, could not update floating actions button, required instances not available");
+			return;
+		}
+
+		if (this.groupService.isGroupOwner(this.groupModel)) {
+			if (this.groupDetailAdapter.getItemCount() > BuildConfig.MAX_GROUP_SIZE) {
+				this.floatingActionButton.hide();
+			} else {
+				this.floatingActionButton.show();
 			}
 			}
 		}
 		}
 	}
 	}
@@ -969,4 +953,66 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 			this.avatarEditView.onRequestPermissionsResult(requestCode, permissions, grantResults);
 			this.avatarEditView.onRequestPermissionsResult(requestCode, permissions, grantResults);
 		}
 		}
 	}
 	}
+
+	@Override
+	public void onGroupOwnerClick(View v, String identity) {
+		Intent intent = new Intent(this, ContactDetailActivity.class);
+		intent.putExtra(ThreemaApplication.INTENT_DATA_CONTACT, groupModel.getCreatorIdentity());
+		intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+		ActivityOptionsCompat options = ActivityOptionsCompat.makeScaleUpAnimation(v, 0, 0, v.getWidth(), v.getHeight());
+		ActivityCompat.startActivityForResult((Activity) this, intent, ThreemaActivity.ACTIVITY_ID_CONTACT_DETAIL, options.toBundle());
+	}
+
+	@Override
+	public void onGroupMemberClick(View v, @NonNull ContactModel contactModel) {
+		String identity = contactModel.getIdentity();
+		String shortName = NameUtil.getShortName(contactModel);
+
+		ArrayList<SelectorDialogItem> items = new ArrayList<>();
+		ArrayList<Integer> optionsMap = new ArrayList<>();
+
+		items.add(new SelectorDialogItem(getString(R.string.show_contact), R.drawable.ic_outline_visibility));
+		optionsMap.add(SELECTOR_OPTION_CONTACT_DETAIL);
+
+		if (!TestUtil.compare(myIdentity, identity)) {
+			items.add(new SelectorDialogItem(String.format(getString(R.string.chat_with), shortName), R.drawable.ic_chat_bubble));
+			optionsMap.add(SELECTOR_OPTION_CHAT);
+
+			if (ContactUtil.canReceiveVoipMessages(contactModel, blackListIdentityService)
+				&& ConfigUtils.isCallsEnabled(GroupDetailActivity.this, preferenceService, licenseService)
+			) {
+				items.add(new SelectorDialogItem(String.format(getString(R.string.call_with), shortName), R.drawable.ic_phone_locked_outline));
+				optionsMap.add(SELECTOR_OPTION_CALL);
+			}
+
+			if (operationMode == MODE_EDIT) {
+				if (groupModel != null && !TestUtil.compare(groupModel.getCreatorIdentity(), identity)) {
+					items.add(new SelectorDialogItem(String.format(getString(R.string.kick_user_from_group), shortName), R.drawable.ic_person_remove_outline));
+					optionsMap.add(SELECTOR_OPTION_REMOVE);
+				}
+			}
+			SelectorDialog selectorDialog = SelectorDialog.newInstance(null, items, null);
+			SelectorInfo selectorInfo = new SelectorInfo();
+			selectorInfo.contactModel = contactModel;
+			selectorInfo.view = v;
+			selectorInfo.optionsMap = optionsMap;
+			selectorDialog.setData(selectorInfo);
+			try {
+				selectorDialog.show(getSupportFragmentManager(), DIALOG_TAG_CHOOSE_ACTION);
+			} catch (IllegalStateException e) {
+				logger.error("Exception", e);
+			}
+		}
+	}
+
+	@Override
+	public void onResetLinkClick() {
+		RuntimeUtil.runOnUiThread(() -> ShowOnceDialog.newInstance(R.string.reset_default_group_link_title, R.string.reset_default_group_link_desc).show(getSupportFragmentManager(), DIALOG_SHOW_ONCE_RESET_LINK_INFO));
+	}
+
+		@Override
+	public void onShareLinkClick() {
+		// option only enabled if there is a default link
+		groupInviteService.shareGroupLink(this, groupInviteService.getDefaultGroupInvite(groupModel).get());
+	}
 }
 }

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

@@ -56,7 +56,6 @@ import java.io.File;
 import java.lang.ref.WeakReference;
 import java.lang.ref.WeakReference;
 import java.net.InetSocketAddress;
 import java.net.InetSocketAddress;
 import java.util.ArrayList;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Date;
 import java.util.Date;
 import java.util.Iterator;
 import java.util.Iterator;
 import java.util.List;
 import java.util.List;
@@ -90,6 +89,7 @@ import ch.threema.app.fragments.ContactsSectionFragment;
 import ch.threema.app.fragments.MessageSectionFragment;
 import ch.threema.app.fragments.MessageSectionFragment;
 import ch.threema.app.fragments.MyIDFragment;
 import ch.threema.app.fragments.MyIDFragment;
 import ch.threema.app.globalsearch.GlobalSearchActivity;
 import ch.threema.app.globalsearch.GlobalSearchActivity;
+import ch.threema.app.grouplinks.OutgoingGroupRequestActivity;
 import ch.threema.app.listeners.AppIconListener;
 import ch.threema.app.listeners.AppIconListener;
 import ch.threema.app.listeners.ContactCountListener;
 import ch.threema.app.listeners.ContactCountListener;
 import ch.threema.app.listeners.ConversationListener;
 import ch.threema.app.listeners.ConversationListener;
@@ -102,6 +102,7 @@ import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.messagereceiver.MessageReceiver;
 import ch.threema.app.messagereceiver.MessageReceiver;
 import ch.threema.app.preference.SettingsActivity;
 import ch.threema.app.preference.SettingsActivity;
 import ch.threema.app.push.PushService;
 import ch.threema.app.push.PushService;
+import ch.threema.app.qrscanner.activity.BaseQrScannerActivity;
 import ch.threema.app.routines.CheckLicenseRoutine;
 import ch.threema.app.routines.CheckLicenseRoutine;
 import ch.threema.app.services.ContactService;
 import ch.threema.app.services.ContactService;
 import ch.threema.app.services.ConversationService;
 import ch.threema.app.services.ConversationService;
@@ -134,10 +135,10 @@ import ch.threema.app.utils.TestUtil;
 import ch.threema.app.voip.activities.CallActivity;
 import ch.threema.app.voip.activities.CallActivity;
 import ch.threema.app.voip.services.VoipCallService;
 import ch.threema.app.voip.services.VoipCallService;
 import ch.threema.app.webclient.activities.SessionsActivity;
 import ch.threema.app.webclient.activities.SessionsActivity;
-import ch.threema.client.ConnectionState;
-import ch.threema.client.ConnectionStateListener;
-import ch.threema.client.LinkMobileNoException;
-import ch.threema.client.ThreemaConnection;
+import ch.threema.domain.protocol.api.LinkMobileNoException;
+import ch.threema.domain.protocol.csp.connection.ConnectionState;
+import ch.threema.domain.protocol.csp.connection.ConnectionStateListener;
+import ch.threema.domain.protocol.csp.connection.ThreemaConnection;
 import ch.threema.localcrypto.MasterKey;
 import ch.threema.localcrypto.MasterKey;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.ContactModel;
@@ -208,9 +209,10 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
 				@Override
 				@Override
 				public void run() {
 				public void run() {
 					if (intent.getAction().equals(IntentDataUtil.ACTION_LICENSE_NOT_ALLOWED)) {
 					if (intent.getAction().equals(IntentDataUtil.ACTION_LICENSE_NOT_ALLOWED)) {
-						if (Arrays.asList(BuildFlavor.LicenseType.SERIAL,
-								BuildFlavor.LicenseType.GOOGLE_WORK,
-								BuildFlavor.LicenseType.HMS_WORK).contains(BuildFlavor.getLicenseType())) {
+						if (BuildFlavor.getLicenseType() == BuildFlavor.LicenseType.SERIAL ||
+							BuildFlavor.getLicenseType() == BuildFlavor.LicenseType.GOOGLE_WORK ||
+							BuildFlavor.getLicenseType() == BuildFlavor.LicenseType.HMS_WORK ||
+							BuildFlavor.getLicenseType() == BuildFlavor.LicenseType.ONPREM) {
 							//show enter serial stuff
 							//show enter serial stuff
 							startActivityForResult(new Intent(HomeActivity.this, EnterSerialActivity.class), ThreemaActivity.ACTIVITY_ID_ENTER_SERIAL);
 							startActivityForResult(new Intent(HomeActivity.this, EnterSerialActivity.class), ThreemaActivity.ACTIVITY_ID_ENTER_SERIAL);
 						} else {
 						} else {
@@ -440,6 +442,13 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
 			updateUnsentMessagesList(removedMessageModel, false);
 			updateUnsentMessagesList(removedMessageModel, false);
 		}
 		}
 
 
+		@Override
+		public void onRemoved(List<AbstractMessageModel> removedMessageModels) {
+			for (AbstractMessageModel removedMessageModel: removedMessageModels) {
+				updateUnsentMessagesList(removedMessageModel, false);
+			}
+		}
+
 		@Override
 		@Override
 		public void onProgressChanged(AbstractMessageModel messageModel, int newProgress) {
 		public void onProgressChanged(AbstractMessageModel messageModel, int newProgress) {
 			//do nothing
 			//do nothing
@@ -806,7 +815,8 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
 	}
 	}
 
 
 	private void showErrorTextAndExit(String text) {
 	private void showErrorTextAndExit(String text) {
-		GenericAlertDialog.newInstance(R.string.error, text, R.string.finish, 0).show(getSupportFragmentManager(), DIALOG_TAG_FINISH_UP);
+		GenericAlertDialog.newInstance(R.string.error, text, R.string.finish, 0)
+			.show(getSupportFragmentManager(), DIALOG_TAG_FINISH_UP);
 	}
 	}
 
 
 	private void runUpdates(final UpdateSystemService updateSystemService) {
 	private void runUpdates(final UpdateSystemService updateSystemService) {
@@ -1234,12 +1244,18 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
 			case R.id.menu_new_distribution_list:
 			case R.id.menu_new_distribution_list:
 				intent = new Intent(this, DistributionListAddActivity.class);
 				intent = new Intent(this, DistributionListAddActivity.class);
 				break;
 				break;
+			case R.id.group_requests:
+				intent = new Intent(this, OutgoingGroupRequestActivity.class);
+				break;
 			case R.id.my_backups:
 			case R.id.my_backups:
 				intent = new Intent(HomeActivity.this, BackupAdminActivity.class);
 				intent = new Intent(HomeActivity.this, BackupAdminActivity.class);
 				break;
 				break;
 			case R.id.webclient:
 			case R.id.webclient:
 				intent = new Intent(HomeActivity.this, SessionsActivity.class);
 				intent = new Intent(HomeActivity.this, SessionsActivity.class);
 				break;
 				break;
+			case R.id.scanner:
+				intent = new Intent(HomeActivity.this, BaseQrScannerActivity.class);
+				break;
 			case R.id.help:
 			case R.id.help:
 				intent = new Intent(HomeActivity.this, SupportActivity.class);
 				intent = new Intent(HomeActivity.this, SupportActivity.class);
 				break;
 				break;
@@ -1361,6 +1377,19 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
 				menuItem.setVisible(false);
 				menuItem.setVisible(false);
 			}
 			}
 
 
+			if (ConfigUtils.supportsGroupLinks()) {
+				MenuItem menuItem = menu.findItem(R.id.scanner);
+				if (menuItem != null) {
+					menuItem.setVisible(true);
+				}
+
+				menuItem = menu.findItem(R.id.group_requests);
+				if (menuItem != null) {
+					menuItem.setVisible(true);
+					menu.setGroupVisible(menuItem.getGroupId(), true);
+				}
+			}
+
 			MenuItem webclientMenuItem = menu.findItem(R.id.webclient);
 			MenuItem webclientMenuItem = menu.findItem(R.id.webclient);
 			if (webclientMenuItem != null) {
 			if (webclientMenuItem != null) {
 				webclientMenuItem.setVisible(!(webDisabled || ConfigUtils.isBlackBerry()));
 				webclientMenuItem.setVisible(!(webDisabled || ConfigUtils.isBlackBerry()));

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

@@ -54,7 +54,7 @@ import ch.threema.app.services.IdListService;
 import ch.threema.app.ui.EmptyRecyclerView;
 import ch.threema.app.ui.EmptyRecyclerView;
 import ch.threema.app.ui.EmptyView;
 import ch.threema.app.ui.EmptyView;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.ConfigUtils;
-import ch.threema.client.ProtocolDefines;
+import ch.threema.domain.protocol.csp.ProtocolDefines;
 import ch.threema.localcrypto.MasterKeyLockedException;
 import ch.threema.localcrypto.MasterKeyLockedException;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.ContactModel;
 
 

+ 3 - 6
app/src/main/java/ch/threema/app/activities/ImagePaintActivity.java

@@ -34,7 +34,6 @@ import android.graphics.Typeface;
 import android.media.FaceDetector;
 import android.media.FaceDetector;
 import android.net.Uri;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.AsyncTask;
-import android.os.Build;
 import android.os.Bundle;
 import android.os.Bundle;
 import android.view.Menu;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.MenuItem;
@@ -133,7 +132,7 @@ public class ImagePaintActivity extends ThreemaToolbarActivity implements Generi
 	public void onBackPressed() {
 	public void onBackPressed() {
 		if (hasChanges()) {
 		if (hasChanges()) {
 			GenericAlertDialog dialogFragment = GenericAlertDialog.newInstance(
 			GenericAlertDialog dialogFragment = GenericAlertDialog.newInstance(
-					R.string.draw,
+					R.string.discard_changes_title,
 					R.string.discard_changes,
 					R.string.discard_changes,
 					R.string.discard,
 					R.string.discard,
 					R.string.cancel);
 					R.string.cancel);
@@ -219,9 +218,7 @@ public class ImagePaintActivity extends ThreemaToolbarActivity implements Generi
 	protected void onCreate(Bundle savedInstanceState) {
 	protected void onCreate(Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
 		super.onCreate(savedInstanceState);
 
 
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-			getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
-		}
+		getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
 
 
 		Intent intent = getIntent();
 		Intent intent = getIntent();
 		MediaItem mediaItem = intent.getParcelableExtra(Intent.EXTRA_STREAM);
 		MediaItem mediaItem = intent.getParcelableExtra(Intent.EXTRA_STREAM);
@@ -672,7 +669,7 @@ public class ImagePaintActivity extends ThreemaToolbarActivity implements Generi
 					try {
 					try {
 						TapTargetView.showFor(this,
 						TapTargetView.showFor(this,
 							TapTarget.forView(v, getString(R.string.face_blur_tooltip_title), getString(R.string.face_blur_tooltip_text))
 							TapTarget.forView(v, getString(R.string.face_blur_tooltip_title), getString(R.string.face_blur_tooltip_text))
-								.outerCircleColor(R.color.accent_dark)      // Specify a color for the outer circle
+								.outerCircleColor(R.color.dark_accent)      // Specify a color for the outer circle
 								.outerCircleAlpha(0.96f)            // Specify the alpha amount for the outer circle
 								.outerCircleAlpha(0.96f)            // Specify the alpha amount for the outer circle
 								.targetCircleColor(android.R.color.white)   // Specify a color for the target circle
 								.targetCircleColor(android.R.color.white)   // Specify a color for the target circle
 								.titleTextSize(24)                  // Specify the size (in sp) of the title text
 								.titleTextSize(24)                  // Specify the size (in sp) of the title text

+ 11 - 18
app/src/main/java/ch/threema/app/activities/MapActivity.java

@@ -28,7 +28,6 @@ import android.content.Context;
 import android.content.Intent;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap;
-import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Color;
 import android.location.Location;
 import android.location.Location;
 import android.location.LocationManager;
 import android.location.LocationManager;
@@ -133,18 +132,14 @@ public class MapActivity extends ThreemaActivity implements GenericAlertDialog.D
 
 
 		setContentView(R.layout.activity_map);
 		setContentView(R.layout.activity_map);
 
 
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-			getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
-			getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
-			getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
-			getWindow().setStatusBarColor(Color.TRANSPARENT);
-			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-				// we want dark icons, i.e. a light status bar
-				getWindow().getDecorView().setSystemUiVisibility(
-						getWindow().getDecorView().getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
-			}
-		} else {
-			getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+		getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+		getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+		getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
+		getWindow().setStatusBarColor(Color.TRANSPARENT);
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+			// we want dark icons, i.e. a light status bar
+			getWindow().getDecorView().setSystemUiVisibility(
+					getWindow().getDecorView().getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
 		}
 		}
 
 
 		try {
 		try {
@@ -240,12 +235,10 @@ public class MapActivity extends ThreemaActivity implements GenericAlertDialog.D
 						}
 						}
 						mapboxMap.addMarker(getMarker(markerPosition, markerName, markerProvider));
 						mapboxMap.addMarker(getMarker(markerPosition, markerName, markerProvider));
 
 
-						if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-							int marginTop = getResources().getDimensionPixelSize(R.dimen.map_compass_margin_top) + insetTop;
-							int marginRight = getResources().getDimensionPixelSize(R.dimen.map_compass_margin_right);
+						int marginTop = getResources().getDimensionPixelSize(R.dimen.map_compass_margin_top) + insetTop;
+						int marginRight = getResources().getDimensionPixelSize(R.dimen.map_compass_margin_right);
 
 
-							mapboxMap.getUiSettings().setCompassMargins(0, marginTop, marginRight, 0);
-						}
+						mapboxMap.getUiSettings().setCompassMargins(0, marginTop, marginRight, 0);
 
 
 						moveCamera(markerPosition, false, -1);
 						moveCamera(markerPosition, false, -1);
 						mapView.postDelayed(new Runnable() {
 						mapView.postDelayed(new Runnable() {

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

@@ -547,7 +547,7 @@ public class MediaGalleryActivity extends ThreemaToolbarActivity implements Adap
 	}
 	}
 
 
 	private void saveMessages() {
 	private void saveMessages() {
-		if (ConfigUtils.requestStoragePermissions(this, null, PERMISSION_REQUEST_SAVE_MESSAGE)) {
+		if (ConfigUtils.requestWriteStoragePermissions(this, null, PERMISSION_REQUEST_SAVE_MESSAGE)) {
 			fileService.saveMedia(this, gridView, new CopyOnWriteArrayList<>(getSelectedMessages()), true);
 			fileService.saveMedia(this, gridView, new CopyOnWriteArrayList<>(getSelectedMessages()), true);
 			actionMode.finish();
 			actionMode.finish();
 		}
 		}

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

@@ -181,8 +181,6 @@ public class MediaViewerActivity extends ThreemaToolbarActivity implements
 		getToolbar().setTitleTextAppearance(this, R.style.TextAppearance_MediaViewer_Title);
 		getToolbar().setTitleTextAppearance(this, R.style.TextAppearance_MediaViewer_Title);
 		getToolbar().setSubtitleTextAppearance(this, R.style.TextAppearance_MediaViewer_SubTitle);
 		getToolbar().setSubtitleTextAppearance(this, R.style.TextAppearance_MediaViewer_SubTitle);
 
 
-		adjustStatusBar();
-
 		this.caption = findViewById(R.id.caption);
 		this.caption = findViewById(R.id.caption);
 
 
 		this.captionContainer = findViewById(R.id.caption_container);
 		this.captionContainer = findViewById(R.id.caption_container);
@@ -194,8 +192,6 @@ public class MediaViewerActivity extends ThreemaToolbarActivity implements
 			return insets;
 			return insets;
 		});
 		});
 
 
-		setCaptionPosition();
-
 		this.currentMessageModel = IntentDataUtil.getAbstractMessageModel(intent, messageService);
 		this.currentMessageModel = IntentDataUtil.getAbstractMessageModel(intent, messageService);
 		try {
 		try {
 			this.currentReceiver = messageService.getMessageReceiver(this.currentMessageModel);
 			this.currentReceiver = messageService.getMessageReceiver(this.currentMessageModel);
@@ -408,7 +404,7 @@ public class MediaViewerActivity extends ThreemaToolbarActivity implements
 			finish();
 			finish();
 			return true;
 			return true;
 		} else if (itemId == R.id.menu_save) {
 		} else if (itemId == R.id.menu_save) {
-			if (ConfigUtils.requestStoragePermissions(this, null, PERMISSION_REQUEST_SAVE_MESSAGE)) {
+			if (ConfigUtils.requestWriteStoragePermissions(this, null, PERMISSION_REQUEST_SAVE_MESSAGE)) {
 				saveMedia();
 				saveMedia();
 			}
 			}
 			return true;
 			return true;
@@ -816,36 +812,10 @@ public class MediaViewerActivity extends ThreemaToolbarActivity implements
 		}
 		}
 	}
 	}
 
 
-	private void setCaptionPosition() {
-		if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
-			try {
-				ConfigUtils.NavigationBarDimensions dimensions = new ConfigUtils.NavigationBarDimensions();
-				dimensions = ConfigUtils.getNavigationBarDimensions(getWindowManager(), dimensions);
-				if (this.captionContainer != null) {
-					FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) this.captionContainer.getLayoutParams();
-					params.setMargins(dimensions.width, 0, dimensions.width, dimensions.height + getResources().getDimensionPixelSize(R.dimen.mediaviewer_caption_border_bottom));
-					this.captionContainer.setLayoutParams(params);
-				}
-			} catch (Exception e) {
-				logger.error("Exception", e);
-			}
-		}
-	}
-
-	private void adjustStatusBar() {
-		if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
-			FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getToolbar().getLayoutParams();
-			lp.topMargin = ConfigUtils.getStatusBarHeight(this);
-			getToolbar().setLayoutParams(lp);
-		}
-	}
-
 	@Override
 	@Override
 	public void onConfigurationChanged(@NonNull Configuration newConfig) {
 	public void onConfigurationChanged(@NonNull Configuration newConfig) {
 		super.onConfigurationChanged(newConfig);
 		super.onConfigurationChanged(newConfig);
 
 
 		ConfigUtils.adjustToolbar(this, getToolbar());
 		ConfigUtils.adjustToolbar(this, getToolbar());
-		adjustStatusBar();
-		setCaptionPosition();
 	}
 	}
 }
 }

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

@@ -127,7 +127,7 @@ public class PinLockActivity extends ThreemaActivity {
 		errorTextView = findViewById(R.id.errorText);
 		errorTextView = findViewById(R.id.errorText);
 
 
 		headerTextView.setText(R.string.confirm_your_pin);
 		headerTextView.setText(R.string.confirm_your_pin);
-		detailsTextView.setText(R.string.pinentry_enter_pin);
+		detailsTextView.setText(getString(R.string.pinentry_enter_pin, getString(R.string.app_name)));
 		passwordEntry.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
 		passwordEntry.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
 
 
 		Button cancelButton = findViewById(R.id.cancelButton);
 		Button cancelButton = findViewById(R.id.cancelButton);

+ 4 - 9
app/src/main/java/ch/threema/app/activities/QRCodeZoomActivity.java

@@ -21,7 +21,6 @@
 
 
 package ch.threema.app.activities;
 package ch.threema.app.activities;
 
 
-import android.os.Build;
 import android.os.Bundle;
 import android.os.Bundle;
 import android.view.View;
 import android.view.View;
 
 
@@ -41,14 +40,10 @@ public class QRCodeZoomActivity extends AppCompatActivity {
 
 
 		final View rootView = getWindow().getDecorView().getRootView();
 		final View rootView = getWindow().getDecorView().getRootView();
 
 
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-			ViewCompat.setOnApplyWindowInsetsListener(rootView, (v, insets) -> {
-				showPopup(v);
-				return insets;
-			});
-		} else {
-			showPopup(rootView);
-		}
+		ViewCompat.setOnApplyWindowInsetsListener(rootView, (v, insets) -> {
+			showPopup(v);
+			return insets;
+		});
 	}
 	}
 
 
 	private void showPopup(final View v) {
 	private void showPopup(final View v) {

+ 30 - 11
app/src/main/java/ch/threema/app/activities/RecipientListBaseActivity.java

@@ -113,7 +113,7 @@ import ch.threema.app.utils.NameUtil;
 import ch.threema.app.utils.NavigationUtil;
 import ch.threema.app.utils.NavigationUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.TestUtil;
-import ch.threema.client.file.FileData;
+import ch.threema.domain.protocol.csp.messages.file.FileData;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.DistributionListModel;
 import ch.threema.storage.models.DistributionListModel;
@@ -400,17 +400,31 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 			}
 			}
 
 
 			String identity = IntentDataUtil.getIdentity(intent);
 			String identity = IntentDataUtil.getIdentity(intent);
-			if (!TestUtil.empty(identity)) {
-				hideUi = true;
+			int groupId = -1;
+			int distributionListID = -1;
+			//check for receiver information either through intent extras or sharing shortcut id which looses its intent extras
+			// (workaround to pass identity extras in shortcut id https://medium.com/@styrc.adam/android-dynamic-shortcuts-passing-extras-64db534621c1)
+			if (TestUtil.empty(identity)) {
+				identity = IntentDataUtil.getIdentityFromSharingShortcut(intent);
+				if (TestUtil.empty(identity)) {
+					groupId = IntentDataUtil.getGroupId(intent);
+
+					if (groupId == -1) {
+						groupId = IntentDataUtil.getGroupIdFromSharingShortcut(intent);
+
+						if (groupId == -1) {
+							// distribution list is only passed as id to recipient activity, no check for intent extras
+							distributionListID = IntentDataUtil.getDistributionListIdFromSharingShortcut(intent);
+						}
+					}
+				}
 			}
 			}
 
 
-			int groupId = IntentDataUtil.getGroupId(intent);
-			if (groupId > 0) {
+			if (groupId > 0 || distributionListID > 0 || !TestUtil.empty(identity)) {
 				hideUi = true;
 				hideUi = true;
 			}
 			}
 
 
 			String action = intent.getAction();
 			String action = intent.getAction();
-
 			if (action != null) {
 			if (action != null) {
 				// called from other app via regular send intent
 				// called from other app via regular send intent
 				if (action.equals(Intent.ACTION_SEND)) {
 				if (action.equals(Intent.ACTION_SEND)) {
@@ -425,7 +439,7 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 						uri = (Uri) parcelable;
 						uri = (Uri) parcelable;
 					}
 					}
 
 
-					if (type != null && (uri != null || MimeUtil.isTextFile(type))) {
+					if (type != null && (uri != null || MimeUtil.isText(type))) {
 						if (type.equals("message/rfc822")) {
 						if (type.equals("message/rfc822")) {
 							// email attachments
 							// email attachments
 							//  extract file type from uri path
 							//  extract file type from uri path
@@ -506,10 +520,12 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 					if (!TestUtil.empty(identity)) {
 					if (!TestUtil.empty(identity)) {
 						prepareForwardingOrSharing(new ArrayList<>(Collections.singletonList(contactService.getByIdentity(identity))));
 						prepareForwardingOrSharing(new ArrayList<>(Collections.singletonList(contactService.getByIdentity(identity))));
 					}
 					}
-
-					if (groupId > 0) {
+					else if (groupId > 0) {
 						prepareForwardingOrSharing(new ArrayList<>(Collections.singletonList(groupService.getById(groupId))));
 						prepareForwardingOrSharing(new ArrayList<>(Collections.singletonList(groupService.getById(groupId))));
 					}
 					}
+					else if (distributionListID > 0) {
+						prepareForwardingOrSharing(new ArrayList<>(Collections.singletonList(distributionListService.getById(distributionListID))));
+					}
 				} else if (action.equals(Intent.ACTION_SENDTO)) {
 				} else if (action.equals(Intent.ACTION_SENDTO)) {
 					// called from contact app or quickcontactbadge
 					// called from contact app or quickcontactbadge
 					if (lockAppService != null && lockAppService.isLocked()) {
 					if (lockAppService != null && lockAppService.isLocked()) {
@@ -900,8 +916,11 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 		if (mediaItems.size() > 0 || originalMessageModels.size() > 0) {
 		if (mediaItems.size() > 0 || originalMessageModels.size() > 0) {
 			String recipientName = "";
 			String recipientName = "";
 
 
-			if (!((mediaItems.size() == 1 && MimeUtil.isTextFile(mediaItems.get(0).getMimeType()))
-				|| (originalMessageModels.size() == 1 && originalMessageModels.get(0).getType() == MessageType.TEXT))) {
+			if (!(
+				(mediaItems.size() == 1 && MimeUtil.isText(mediaItems.get(0).getMimeType()) && !MimeUtil.isFileType(mediaItems.get(0).getType())) // not a single plain text item (.txt file should == true bc. mimetype text/plain but type file)
+				||
+				(originalMessageModels.size() == 1 && originalMessageModels.get(0).getType() == MessageType.TEXT)) // not a single threema text message
+			) {
 				for (Object model : recipients) {
 				for (Object model : recipients) {
 					if (recipientName.length() > 0) {
 					if (recipientName.length() > 0) {
 						recipientName += ", ";
 						recipientName += ", ";

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

@@ -38,13 +38,11 @@ import android.graphics.PorterDuff;
 import android.media.MediaMetadataRetriever;
 import android.media.MediaMetadataRetriever;
 import android.net.Uri;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.AsyncTask;
-import android.os.Build;
 import android.os.Bundle;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Handler;
 import android.provider.MediaStore;
 import android.provider.MediaStore;
 import android.text.Editable;
 import android.text.Editable;
 import android.text.TextWatcher;
 import android.text.TextWatcher;
-import android.util.DisplayMetrics;
 import android.view.KeyEvent;
 import android.view.KeyEvent;
 import android.view.Menu;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.MenuItem;
@@ -188,41 +186,21 @@ public class SendMediaActivity extends ThreemaToolbarActivity implements
 		}
 		}
 
 
 		if (preferenceService.getEmojiStyle() != PreferenceService.EmojiStyle_ANDROID) {
 		if (preferenceService.getEmojiStyle() != PreferenceService.EmojiStyle_ANDROID) {
-			if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
-				findViewById(R.id.activity_parent).getRootView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
-					@Override
-					public void onGlobalLayout() {
-						DisplayMetrics metrics = new DisplayMetrics();
-						// get dimensions of usable display space with decorations (status bar / navigation bar) subtracted
-						getWindowManager().getDefaultDisplay().getMetrics(metrics);
-						int usableHeight = metrics.heightPixels;
-						int statusBarHeight = ConfigUtils.getStatusBarHeight(SendMediaActivity.this);
-						int rootViewHeight = findViewById(R.id.activity_parent).getHeight();
-
-						if (rootViewHeight + statusBarHeight == usableHeight) {
-							onSoftKeyboardClosed();
-						} else {
-							onSoftKeyboardOpened(usableHeight - statusBarHeight - rootViewHeight);
-						}
-					}
-				});
-			} else {
-				ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.activity_parent).getRootView(), new OnApplyWindowInsetsListener() {
-					@Override
-					public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
+			ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.activity_parent).getRootView(), new OnApplyWindowInsetsListener() {
+				@Override
+				public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
 
 
-						logger.debug("system window top " + insets.getSystemWindowInsetTop() + " bottom " + insets.getSystemWindowInsetBottom());
-						logger.debug("stable insets top " + insets.getStableInsetTop() + " bottom " + insets.getStableInsetBottom());
+					logger.debug("system window top " + insets.getSystemWindowInsetTop() + " bottom " + insets.getSystemWindowInsetBottom());
+					logger.debug("stable insets top " + insets.getStableInsetTop() + " bottom " + insets.getStableInsetBottom());
 
 
-						if (insets.getSystemWindowInsetBottom() <= insets.getStableInsetBottom()) {
-							onSoftKeyboardClosed();
-						} else {
-							onSoftKeyboardOpened(insets.getSystemWindowInsetBottom() - insets.getStableInsetBottom());
-						}
-						return insets;
+					if (insets.getSystemWindowInsetBottom() <= insets.getStableInsetBottom()) {
+						onSoftKeyboardClosed();
+					} else {
+						onSoftKeyboardOpened(insets.getSystemWindowInsetBottom() - insets.getStableInsetBottom());
 					}
 					}
-				});
-			}
+					return insets;
+				}
+			});
 			addOnSoftKeyboardChangedListener(this);
 			addOnSoftKeyboardChangedListener(this);
 		}
 		}
 
 
@@ -1201,13 +1179,11 @@ public class SendMediaActivity extends ThreemaToolbarActivity implements
 	}
 	}
 
 
 	private void showBigVideo(MediaItem item) {
 	private void showBigVideo(MediaItem item) {
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-			this.bigImageView.setVisibility(View.GONE);
-			this.bigGifImageView.setVisibility(View.GONE);
-			this.videoEditView.setVisibility(View.VISIBLE);
-			this.videoEditView.setVideo(item);
-			logger.debug("show video " + item.getDurationMs());
-		}
+		this.bigImageView.setVisibility(View.GONE);
+		this.bigGifImageView.setVisibility(View.GONE);
+		this.videoEditView.setVisibility(View.VISIBLE);
+		this.videoEditView.setVideo(item);
+		logger.debug("show video " + item.getDurationMs());
 	}
 	}
 
 
 	private void showBigImage(final int position) {
 	private void showBigImage(final int position) {
@@ -1303,7 +1279,7 @@ public class SendMediaActivity extends ThreemaToolbarActivity implements
 	private void confirmQuit() {
 	private void confirmQuit() {
 		if (hasChanges) {
 		if (hasChanges) {
 			GenericAlertDialog dialogFragment = GenericAlertDialog.newInstance(
 			GenericAlertDialog dialogFragment = GenericAlertDialog.newInstance(
-					R.string.send_media,
+					R.string.discard_changes_title,
 					R.string.discard_changes,
 					R.string.discard_changes,
 					R.string.yes,
 					R.string.yes,
 					R.string.no);
 					R.string.no);

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

@@ -35,7 +35,7 @@ import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.services.NotificationService;
 import ch.threema.app.services.NotificationService;
 import ch.threema.app.services.PassphraseService;
 import ch.threema.app.services.PassphraseService;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.ConfigUtils;
-import ch.threema.client.ThreemaConnection;
+import ch.threema.domain.protocol.csp.connection.ThreemaConnection;
 import ch.threema.localcrypto.MasterKey;
 import ch.threema.localcrypto.MasterKey;
 
 
 /**
 /**

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

@@ -163,20 +163,12 @@ public class TextChatBubbleActivity extends ThreemaActivity implements GenericAl
 
 
 		if (messageModel.isOutbox()) {
 		if (messageModel.isOutbox()) {
 			// send
 			// send
-			if (ConfigUtils.getAppTheme(this) == ConfigUtils.THEME_DARK) {
-				color = getResources().getColor(R.color.dark_bubble_send);
-			} else {
-				color = getResources().getColor(R.color.light_bubble_send);
-			}
+			color = ConfigUtils.getColorFromAttribute(this, R.attr.bubble_send);
 			title = getString(R.string.threema_message_to, messageReceiver.getDisplayName());
 			title = getString(R.string.threema_message_to, messageReceiver.getDisplayName());
 			footerLayout = R.layout.conversation_bubble_footer_send;
 			footerLayout = R.layout.conversation_bubble_footer_send;
 		} else {
 		} else {
 			// recv
 			// recv
-			if (ConfigUtils.getAppTheme(this) == ConfigUtils.THEME_DARK) {
-				color = getResources().getColor(R.color.dark_bubble_recv);
-			} else {
-				color = getResources().getColor(R.color.light_bubble_recv);
-			}
+			color = ConfigUtils.getColorFromAttribute(this, R.attr.bubble_recv);
 			title = getString(R.string.threema_message_from, messageReceiver.getDisplayName());
 			title = getString(R.string.threema_message_from, messageReceiver.getDisplayName());
 			footerLayout = R.layout.conversation_bubble_footer_recv;
 			footerLayout = R.layout.conversation_bubble_footer_recv;
 		}
 		}

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

@@ -68,6 +68,7 @@ public abstract class ThreemaActivity extends ThreemaAppCompatActivity {
 	public static final int ACTIVITY_ID_PICK_FILE = 20047;
 	public static final int ACTIVITY_ID_PICK_FILE = 20047;
 	public static final int ACTIVITY_ID_PAINT = 20049;
 	public static final int ACTIVITY_ID_PAINT = 20049;
 	public static final int ACTIVITY_ID_PICK_MEDIA = 20050;
 	public static final int ACTIVITY_ID_PICK_MEDIA = 20050;
+	public static final int ACTIVITY_ID_MANAGE_GROUP_LINKS = 20051;
 
 
 	public static final int RESULT_RESTART = 40005;
 	public static final int RESULT_RESTART = 40005;
 
 

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

@@ -54,9 +54,9 @@ import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.ConnectionIndicatorUtil;
 import ch.threema.app.utils.ConnectionIndicatorUtil;
 import ch.threema.app.utils.EditTextUtil;
 import ch.threema.app.utils.EditTextUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.RuntimeUtil;
-import ch.threema.client.ConnectionState;
-import ch.threema.client.ConnectionStateListener;
-import ch.threema.client.ThreemaConnection;
+import ch.threema.domain.protocol.csp.connection.ConnectionState;
+import ch.threema.domain.protocol.csp.connection.ConnectionStateListener;
+import ch.threema.domain.protocol.csp.connection.ThreemaConnection;
 import ch.threema.localcrypto.MasterKey;
 import ch.threema.localcrypto.MasterKey;
 
 
 /**
 /**
@@ -134,7 +134,7 @@ public abstract class ThreemaToolbarActivity extends ThreemaActivity implements
 		}
 		}
 	}
 	}
 
 
-	private void initServices() {
+	protected void initServices() {
 		if (serviceManager == null) {
 		if (serviceManager == null) {
 			serviceManager = ThreemaApplication.getServiceManager();
 			serviceManager = ThreemaApplication.getServiceManager();
 			if (serviceManager == null) {
 			if (serviceManager == null) {

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

@@ -22,12 +22,9 @@
 package ch.threema.app.activities;
 package ch.threema.app.activities;
 
 
 import android.content.Intent;
 import android.content.Intent;
-import android.content.res.Configuration;
 import android.os.Bundle;
 import android.os.Bundle;
-import android.text.Html;
 import android.view.View;
 import android.view.View;
 import android.widget.LinearLayout;
 import android.widget.LinearLayout;
-import android.widget.TextView;
 
 
 import ch.threema.app.R;
 import ch.threema.app.R;
 import ch.threema.app.utils.AnimationUtil;
 import ch.threema.app.utils.AnimationUtil;
@@ -38,15 +35,14 @@ import static ch.threema.app.activities.WhatsNewActivity.EXTRA_NO_ANIMATION;
 public class WhatsNew2Activity extends ThreemaAppCompatActivity {
 public class WhatsNew2Activity extends ThreemaAppCompatActivity {
 	@Override
 	@Override
 	protected void onCreate(Bundle savedInstanceState) {
 	protected void onCreate(Bundle savedInstanceState) {
-
 		ConfigUtils.configureActivityTheme(this);
 		ConfigUtils.configureActivityTheme(this);
 
 
 		super.onCreate(savedInstanceState);
 		super.onCreate(savedInstanceState);
 
 
 		setContentView(R.layout.activity_whatsnew2);
 		setContentView(R.layout.activity_whatsnew2);
-
+/*
 		((TextView) findViewById(R.id.whatsnew_body)).setText(Html.fromHtml(getString(R.string.whatsnew2_body, getString(R.string.app_name))));
 		((TextView) findViewById(R.id.whatsnew_body)).setText(Html.fromHtml(getString(R.string.whatsnew2_body, getString(R.string.app_name))));
-
+*/
 		findViewById(R.id.ok_button).setOnClickListener(v -> {
 		findViewById(R.id.ok_button).setOnClickListener(v -> {
 			finish();
 			finish();
 			overridePendingTransition(R.anim.slide_in_right_short, R.anim.slide_out_left_short);
 			overridePendingTransition(R.anim.slide_in_right_short, R.anim.slide_out_left_short);
@@ -61,11 +57,6 @@ public class WhatsNew2Activity extends ThreemaAppCompatActivity {
 		}
 		}
 	}
 	}
 
 
-	@Override
-	public void onConfigurationChanged(Configuration newConfig) {
-		super.onConfigurationChanged(newConfig);
-	}
-
 	@Override
 	@Override
 	public void onBackPressed() {
 	public void onBackPressed() {
 		Intent intent = new Intent(WhatsNew2Activity.this, WhatsNewActivity.class);
 		Intent intent = new Intent(WhatsNew2Activity.this, WhatsNewActivity.class);

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

@@ -21,11 +21,10 @@
 
 
 package ch.threema.app.activities;
 package ch.threema.app.activities;
 
 
-import android.content.res.Configuration;
+import android.content.Intent;
 import android.os.Bundle;
 import android.os.Bundle;
 import android.view.View;
 import android.view.View;
 import android.widget.LinearLayout;
 import android.widget.LinearLayout;
-import android.widget.TextView;
 
 
 import ch.threema.app.R;
 import ch.threema.app.R;
 import ch.threema.app.utils.AnimationUtil;
 import ch.threema.app.utils.AnimationUtil;
@@ -42,13 +41,13 @@ public class WhatsNewActivity extends ThreemaAppCompatActivity {
 		super.onCreate(savedInstanceState);
 		super.onCreate(savedInstanceState);
 
 
 		setContentView(R.layout.activity_whatsnew);
 		setContentView(R.layout.activity_whatsnew);
-
+/*
 		((TextView) findViewById(R.id.whatsnew_title)).setText(getString(R.string.whatsnew_title, getString(R.string.app_name)));
 		((TextView) findViewById(R.id.whatsnew_title)).setText(getString(R.string.whatsnew_title, getString(R.string.app_name)));
 		((TextView) findViewById(R.id.whatsnew_body)).setText(getString(R.string.whatsnew_headline, getString(R.string.app_name)));
 		((TextView) findViewById(R.id.whatsnew_body)).setText(getString(R.string.whatsnew_headline, getString(R.string.app_name)));
-
+*/
 		findViewById(R.id.next_text).setOnClickListener(v -> {
 		findViewById(R.id.next_text).setOnClickListener(v -> {
-//			startActivity(new Intent(WhatsNewActivity.this, WhatsNew2Activity.class));
-//			overridePendingTransition(R.anim.slide_in_right_short, R.anim.slide_out_left_short);
+			startActivity(new Intent(WhatsNewActivity.this, WhatsNew2Activity.class));
+			overridePendingTransition(R.anim.slide_in_right_short, R.anim.slide_out_left_short);
 			finish();
 			finish();
 		});
 		});
 
 
@@ -60,9 +59,4 @@ public class WhatsNewActivity extends ThreemaAppCompatActivity {
 			}
 			}
 		}
 		}
 	}
 	}
-
-	@Override
-	public void onConfigurationChanged(Configuration newConfig) {
-		super.onConfigurationChanged(newConfig);
-	}
 }
 }

+ 53 - 9
app/src/main/java/ch/threema/app/activities/ballot/BallotMatrixActivity.java

@@ -30,9 +30,14 @@ import android.widget.TableRow;
 import android.widget.TextView;
 import android.widget.TextView;
 import android.widget.Toast;
 import android.widget.Toast;
 
 
+import com.google.android.material.card.MaterialCardView;
+
 import org.slf4j.Logger;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.slf4j.LoggerFactory;
 
 
+import java.util.ArrayList;
+import java.util.List;
+
 import androidx.appcompat.app.ActionBar;
 import androidx.appcompat.app.ActionBar;
 import ch.threema.app.R;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.ThreemaApplication;
@@ -68,8 +73,9 @@ public class BallotMatrixActivity extends BallotDetailActivity {
 	private ContactService contactService;
 	private ContactService contactService;
 	private GroupService groupService;
 	private GroupService groupService;
 	private String identity;
 	private String identity;
+	private View scrollParent, noVotesView;
 
 
-	private BallotVoteListener ballotVoteListener = new BallotVoteListener() {
+	private final BallotVoteListener ballotVoteListener = new BallotVoteListener() {
 		@Override
 		@Override
 		public void onSelfVote(BallotModel ballotModel) {
 		public void onSelfVote(BallotModel ballotModel) {
 			ballotListener.onModified(ballotModel);
 			ballotListener.onModified(ballotModel);
@@ -91,7 +97,7 @@ public class BallotMatrixActivity extends BallotDetailActivity {
 		}
 		}
 	};
 	};
 
 
-	private BallotListener ballotListener = new BallotListener() {
+	private final BallotListener ballotListener = new BallotListener() {
 		@Override
 		@Override
 		public void onClosed(BallotModel ballotModel) {
 		public void onClosed(BallotModel ballotModel) {
 			this.onModified(ballotModel);
 			this.onModified(ballotModel);
@@ -165,6 +171,9 @@ public class BallotMatrixActivity extends BallotDetailActivity {
 			textView.setText(this.getBallotModel().getName());
 			textView.setText(this.getBallotModel().getName());
 		}
 		}
 
 
+		noVotesView = findViewById(R.id.no_votes_yet);
+		scrollParent = findViewById(R.id.scroll_parent);
+
 		ListenerManager.ballotListeners.add(this.ballotListener);
 		ListenerManager.ballotListeners.add(this.ballotListener);
 		ListenerManager.ballotVoteListeners.add(this.ballotVoteListener);
 		ListenerManager.ballotVoteListeners.add(this.ballotVoteListener);
 		this.updateView();
 		this.updateView();
@@ -175,8 +184,7 @@ public class BallotMatrixActivity extends BallotDetailActivity {
 		return R.layout.activity_ballot_matrix;
 		return R.layout.activity_ballot_matrix;
 	}
 	}
 
 
-
-	private void refreshList() {
+	private void updateView() {
 		TableLayout dataTableLayout = findViewById(R.id.matrix_data);
 		TableLayout dataTableLayout = findViewById(R.id.matrix_data);
 
 
 		if(dataTableLayout != null) {
 		if(dataTableLayout != null) {
@@ -192,6 +200,28 @@ public class BallotMatrixActivity extends BallotDetailActivity {
 			return;
 			return;
 		}
 		}
 
 
+		List<BallotMatrixService.Participant> allParticipants = matrixData.getParticipants();
+		List<BallotMatrixService.Participant> votedParticipants = new ArrayList<>();
+		List<BallotMatrixService.Participant> notVotedParticipants = new ArrayList<>();
+
+		for (BallotMatrixService.Participant participant: allParticipants) {
+			if (participant.hasVoted()) {
+				votedParticipants.add(participant);
+			} else {
+				notVotedParticipants.add(participant);
+			}
+		}
+
+		if (votedParticipants.size() == 0) {
+			// no votes
+			noVotesView.setVisibility(View.VISIBLE);
+			scrollParent.setVisibility(View.GONE);
+			return;
+		}
+
+		noVotesView.setVisibility(View.GONE);
+		scrollParent.setVisibility(View.VISIBLE);
+
 		// add header row containing names/avatars of participants
 		// add header row containing names/avatars of participants
 		TableRow nameHeaderRow = new TableRow(this);
 		TableRow nameHeaderRow = new TableRow(this);
 
 
@@ -201,7 +231,7 @@ public class BallotMatrixActivity extends BallotDetailActivity {
 		View emptyCell2 = getLayoutInflater().inflate(R.layout.row_cell_ballot_matrix_empty, null);
 		View emptyCell2 = getLayoutInflater().inflate(R.layout.row_cell_ballot_matrix_empty, null);
 		nameHeaderRow.addView(emptyCell2);
 		nameHeaderRow.addView(emptyCell2);
 
 
-		for(BallotMatrixService.Participant p: matrixData.getParticipants()) {
+		for(BallotMatrixService.Participant p: votedParticipants) {
 			final ContactModel contactModel = this.contactService.getByIdentity(p.getIdentity());
 			final ContactModel contactModel = this.contactService.getByIdentity(p.getIdentity());
 
 
 			View nameCell = getLayoutInflater().inflate(R.layout.row_cell_ballot_matrix_name, null);
 			View nameCell = getLayoutInflater().inflate(R.layout.row_cell_ballot_matrix_name, null);
@@ -243,7 +273,7 @@ public class BallotMatrixActivity extends BallotDetailActivity {
 
 
 			row.addView(sumCell);
 			row.addView(sumCell);
 
 
-			for (BallotMatrixService.Participant p: matrixData.getParticipants()) {
+			for (BallotMatrixService.Participant p: votedParticipants) {
 				View choiceVoteView;
 				View choiceVoteView;
 
 
 				if (c.isWinner()) {
 				if (c.isWinner()) {
@@ -271,10 +301,24 @@ public class BallotMatrixActivity extends BallotDetailActivity {
 
 
 			dataTableLayout.addView(row);
 			dataTableLayout.addView(row);
 		}
 		}
-	}
 
 
-	private void updateView() {
-		this.refreshList();
+		TextView notVotedTextView = findViewById(R.id.not_voted);
+		MaterialCardView notVotedContainer = findViewById(R.id.not_voted_container);
+
+		if (contactService != null && notVotedParticipants.size() > 0) {
+			notVotedContainer.setVisibility(View.VISIBLE);
+			String userList = "";
+
+			for (BallotMatrixService.Participant p: notVotedParticipants) {
+				if (!"".equals(userList)) {
+					userList += ", ";
+				}
+				userList += NameUtil.getDisplayNameOrNickname(p.getIdentity(), contactService);
+			}
+			notVotedTextView.setText(getString(R.string.not_voted_user_list, userList));
+ 		} else {
+			notVotedContainer.setVisibility(View.GONE);
+		}
 	}
 	}
 
 
 	@Override
 	@Override

+ 5 - 4
app/src/main/java/ch/threema/app/activities/ballot/BallotOverviewActivity.java

@@ -55,6 +55,7 @@ import ch.threema.app.services.ContactService;
 import ch.threema.app.services.GroupService;
 import ch.threema.app.services.GroupService;
 import ch.threema.app.services.ballot.BallotService;
 import ch.threema.app.services.ballot.BallotService;
 import ch.threema.app.ui.EmptyView;
 import ch.threema.app.ui.EmptyView;
+import ch.threema.app.ui.SelectorDialogItem;
 import ch.threema.app.utils.BallotUtil;
 import ch.threema.app.utils.BallotUtil;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.IntentDataUtil;
 import ch.threema.app.utils.IntentDataUtil;
@@ -273,19 +274,19 @@ public class BallotOverviewActivity extends ThreemaToolbarActivity implements Li
 			BallotModel ballotModel = listAdapter.getItem(position);
 			BallotModel ballotModel = listAdapter.getItem(position);
 
 
 			if (ballotModel != null) {
 			if (ballotModel != null) {
-				ArrayList<String> items = new ArrayList<>(3);
+				ArrayList<SelectorDialogItem> items = new ArrayList<>(3);
 				ArrayList<Integer> values = new ArrayList<>(3);
 				ArrayList<Integer> values = new ArrayList<>(3);
 
 
 				if (BallotUtil.canVote(ballotModel, myIdentity)) {
 				if (BallotUtil.canVote(ballotModel, myIdentity)) {
-					items.add(getString(R.string.ballot_vote));
+					items.add(new SelectorDialogItem(getString(R.string.ballot_vote), R.drawable.ic_vote_outline));
 					values.add(SELECTOR_ID_VOTE);
 					values.add(SELECTOR_ID_VOTE);
 				}
 				}
 				if (BallotUtil.canViewMatrix(ballotModel, myIdentity)) {
 				if (BallotUtil.canViewMatrix(ballotModel, myIdentity)) {
-					items.add(getString(ballotModel.getState() == BallotModel.State.CLOSED ? R.string.ballot_result_final : R.string.ballot_result_intermediate));
+					items.add(new SelectorDialogItem(getString(ballotModel.getState() == BallotModel.State.CLOSED ? R.string.ballot_result_final : R.string.ballot_result_intermediate), R.drawable.ic_ballot_outline));
 					values.add(SELECTOR_ID_RESULTS);
 					values.add(SELECTOR_ID_RESULTS);
 				}
 				}
 				if (BallotUtil.canClose(ballotModel, myIdentity)) {
 				if (BallotUtil.canClose(ballotModel, myIdentity)) {
-					items.add(getString(R.string.ballot_close));
+					items.add(new SelectorDialogItem(getString(R.string.ballot_close), R.drawable.ic_check));
 					values.add(SELECTOR_ID_CLOSE);
 					values.add(SELECTOR_ID_CLOSE);
 				}
 				}
 
 

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

@@ -57,7 +57,7 @@ import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.IntentDataUtil;
 import ch.threema.app.utils.IntentDataUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.base.ThreemaException;
 import ch.threema.base.ThreemaException;
-import ch.threema.client.APIConnector;
+import ch.threema.domain.protocol.api.APIConnector;
 import ch.threema.storage.models.ballot.BallotChoiceModel;
 import ch.threema.storage.models.ballot.BallotChoiceModel;
 import ch.threema.storage.models.ballot.BallotModel;
 import ch.threema.storage.models.ballot.BallotModel;
 
 

+ 338 - 0
app/src/main/java/ch/threema/app/activities/wizard/WizardBackupRestoreActivity.java

@@ -0,0 +1,338 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2021 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.app.activities.wizard;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.Html;
+import android.text.method.LinkMovementMethod;
+import android.widget.TextView;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.core.content.ContextCompat;
+import ch.threema.app.R;
+import ch.threema.app.ThreemaApplication;
+import ch.threema.app.activities.DisableBatteryOptimizationsActivity;
+import ch.threema.app.activities.ThreemaActivity;
+import ch.threema.app.activities.ThreemaAppCompatActivity;
+import ch.threema.app.backuprestore.csv.RestoreService;
+import ch.threema.app.dialogs.GenericAlertDialog;
+import ch.threema.app.dialogs.GenericProgressDialog;
+import ch.threema.app.dialogs.PasswordEntryDialog;
+import ch.threema.app.managers.ServiceManager;
+import ch.threema.app.services.FileService;
+import ch.threema.app.services.PreferenceService;
+import ch.threema.app.services.UserService;
+import ch.threema.app.threemasafe.ThreemaSafeMDMConfig;
+import ch.threema.app.utils.ConfigUtils;
+import ch.threema.app.utils.DialogUtil;
+import ch.threema.app.utils.FileUtil;
+import ch.threema.app.utils.LocaleUtil;
+import ch.threema.app.utils.MimeUtil;
+import ch.threema.app.utils.RuntimeUtil;
+import ch.threema.app.utils.TestUtil;
+
+public class WizardBackupRestoreActivity extends ThreemaAppCompatActivity implements GenericAlertDialog.DialogClickListener,
+	PasswordEntryDialog.PasswordEntryDialogClickListener {
+	private static final Logger logger = LoggerFactory.getLogger(WizardBackupRestoreActivity.class);
+
+	private static final String DIALOG_TAG_DISABLE_ENERGYSAVE_CONFIRM = "de";
+	private static final String DIALOG_TAG_DOWNLOADING_BACKUP = "dwnldBkp";
+	private static final String DIALOG_TAG_NO_INTERNET = "nin";
+
+	public static final int REQUEST_ID_DISABLE_BATTERY_OPTIMIZATIONS = 541;
+
+	private ThreemaSafeMDMConfig safeMDMConfig;
+	private FileService fileService;
+	private UserService userService;
+	private PreferenceService preferenceService;
+
+	@Override
+	protected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+
+		// directly forward to ID restore activity
+		Intent intent = getIntent();
+		if (intent.hasExtra(ThreemaApplication.INTENT_DATA_ID_BACKUP) &&
+			intent.hasExtra(ThreemaApplication.INTENT_DATA_ID_BACKUP_PW)) {
+
+			restoreIDExport(intent.getStringExtra(ThreemaApplication.INTENT_DATA_ID_BACKUP),
+				intent.getStringExtra(ThreemaApplication.INTENT_DATA_ID_BACKUP_PW));
+		}
+
+		initServices();
+		initLayout();
+		initListeners();
+	}
+
+	@Override
+	protected void onPause() {
+		ThreemaApplication.activityPaused(this);
+		super.onPause();
+	}
+
+	@Override
+	protected void onResume() {
+		ThreemaApplication.activityResumed(this);
+		super.onResume();
+	}
+
+	@Override
+	public void onUserInteraction() {
+		ThreemaApplication.activityUserInteract(this);
+		super.onUserInteraction();
+	}
+
+	private void initServices() {
+		this.safeMDMConfig = ThreemaSafeMDMConfig.getInstance();
+
+		try {
+			ServiceManager serviceManager = ThreemaApplication.getServiceManager();
+			if (serviceManager != null) {
+				fileService = serviceManager.getFileService();
+				userService = serviceManager.getUserService();
+				preferenceService = serviceManager.getPreferenceService();
+			}
+		} catch (Exception e) {
+			logger.error("Exception ", e);
+			finish();
+		}
+	}
+
+	private void initLayout() {
+		setContentView(R.layout.activity_backup_restore);
+
+		String faqURL = String.format(getString(R.string.backup_faq_url), LocaleUtil.getAppLanguage());
+		TextView backupSubtitle = findViewById(R.id.backup_restore_subtitle);
+		backupSubtitle.setText(Html.fromHtml(
+			String.format(getString(R.string.backup_restore_type), faqURL))
+		);
+		backupSubtitle.setMovementMethod(LinkMovementMethod.getInstance());
+
+		if (ConfigUtils.isWorkRestricted()) {
+			if (safeMDMConfig.isRestoreDisabled()) {
+				findViewById(R.id.safe_backup).setEnabled(false);
+			}
+		}
+	}
+
+	private void initListeners() {
+		findViewById(R.id.safe_backup).setOnClickListener(v -> restoreSafe());
+		findViewById(R.id.data_backup).setOnClickListener(v -> showDisableEnergySaveDialog());
+		findViewById(R.id.id_backup).setOnClickListener(v -> restoreIDExport(null, null));
+		findViewById(R.id.cancel).setOnClickListener(v -> finish());
+	}
+
+	public void restoreSafe() {
+		startActivity(new Intent(this, WizardSafeRestoreActivity.class));
+		overridePendingTransition(R.anim.abc_fade_in, R.anim.abc_fade_out);
+	}
+
+	public void restoreIDExport(String backupString, String backupPassword) {
+		Intent intent = new Intent(this, WizardIDRestoreActivity.class);
+
+		if (!TestUtil.empty(backupString) && !TestUtil.empty(backupPassword)) {
+			intent.putExtra(ThreemaApplication.INTENT_DATA_ID_BACKUP, backupString);
+			intent.putExtra(ThreemaApplication.INTENT_DATA_ID_BACKUP_PW, backupPassword);
+		}
+		startActivityForResult(intent, ThreemaActivity.ACTIVITY_ID_RESTORE_KEY);
+		overridePendingTransition(R.anim.abc_fade_in, R.anim.abc_fade_out);
+	}
+
+	private void restoreBackup(final Uri uri) {
+		if (!ContentResolver.SCHEME_FILE.equalsIgnoreCase(uri.getScheme()) && this.fileService != null) {
+			// copy "file" to cache directory first
+			GenericProgressDialog.newInstance(R.string.importing_files, R.string.please_wait).show(getSupportFragmentManager(), DIALOG_TAG_DOWNLOADING_BACKUP);
+
+			new Thread(() -> {
+				final File file = fileService.copyUriToTempFile(uri, "file", "zip", true);
+
+				RuntimeUtil.runOnUiThread(() -> {
+					DialogUtil.dismissDialog(getSupportFragmentManager(), DIALOG_TAG_DOWNLOADING_BACKUP, true);
+
+					if (file != null) {
+						restoreBackupFile(file);
+						file.deleteOnExit();
+					}
+				});
+			}).start();
+
+		} else {
+			String path = FileUtil.getRealPathFromURI(this, uri);
+			if (path != null && !path.isEmpty()) {
+				File file = new File(path);
+				if (file.exists()) {
+					restoreBackupFile(file);
+				}
+			}
+		}
+	}
+
+	private void restoreBackupFile(File file) {
+		if (file.exists()) {
+//			try {
+// Zipfile validity check is sometimes wrong
+//				ZipFile zipFile = new ZipFile(file);
+//				if (zipFile.isValidZipFile()) {
+			ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
+
+			NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
+			if (activeNetwork == null || !activeNetwork.isConnectedOrConnecting()) {
+				showNoInternetDialog(file);
+			} else {
+				confirmRestore(file);
+			}
+			return;
+		}
+//				}
+//			} catch (ZipException e) {
+//				logger.error("Exception", e);
+//			}
+		logger.error(getString(R.string.invalid_backup), this);
+	}
+
+	private void showDisableEnergySaveDialog() {
+		GenericAlertDialog.newInstance(R.string.menu_restore, R.string.restore_disable_energy_saving, R.string.ok, R.string.cancel).show(getSupportFragmentManager(), DIALOG_TAG_DISABLE_ENERGYSAVE_CONFIRM);
+	}
+
+	private void confirmRestore(File file) {
+		PasswordEntryDialog dialogFragment = PasswordEntryDialog.newInstance(
+			R.string.backup_data_title,
+			R.string.restore_data_password_msg,
+			R.string.password_hint,
+			R.string.ok,
+			R.string.cancel,
+			ThreemaApplication.MIN_PW_LENGTH_BACKUP,
+			ThreemaApplication.MAX_PW_LENGTH_BACKUP,
+			0, 0, 0, PasswordEntryDialog.ForgotHintType.PIN_PASSPHRASE);
+		dialogFragment.setData(file);
+		dialogFragment.show(getSupportFragmentManager(), "restorePW");
+	}
+
+	@UiThread
+	private void showNoInternetDialog(File file) {
+		GenericAlertDialog dialog = GenericAlertDialog.newInstance(R.string.menu_restore, R.string.new_wizard_need_internet, R.string.retry, R.string.cancel);
+		dialog.setData(file);
+		dialog.show(getSupportFragmentManager(), DIALOG_TAG_NO_INTERNET);
+	}
+
+	// start generic alert dialog callbacks
+	@Override
+	public void onYes(String tag, Object data) {
+		switch (tag) {
+			case DIALOG_TAG_DISABLE_ENERGYSAVE_CONFIRM:
+				Intent intent = new Intent(this, DisableBatteryOptimizationsActivity.class);
+				intent.putExtra(DisableBatteryOptimizationsActivity.EXTRA_NAME, getString(R.string.restore));
+				intent.putExtra(DisableBatteryOptimizationsActivity.EXTRA_WIZARD, true);
+				startActivityForResult(intent, REQUEST_ID_DISABLE_BATTERY_OPTIMIZATIONS);
+				break;
+			case DIALOG_TAG_NO_INTERNET:
+				restoreBackupFile((File) data);
+				break;
+		}
+	}
+
+	@Override
+	public void onNo(String tag, Object data) {
+		if (safeMDMConfig.isRestoreDisabled()) {
+			finish();
+		}
+	}
+	// end generic alert dialog callbacks
+
+	// start password dialog callbacks
+	@Override
+	public void onYes(String tag, String text, boolean isChecked, Object data) {
+		Intent intent = new Intent(this, RestoreService.class);
+		intent.putExtra(RestoreService.EXTRA_RESTORE_BACKUP_FILE, (File) data);
+		intent.putExtra(RestoreService.EXTRA_RESTORE_BACKUP_PASSWORD, text);
+		ContextCompat.startForegroundService(this, intent);
+		finish();
+	}
+
+	@Override
+	public void onNo(String tag) {
+		if (safeMDMConfig.isRestoreDisabled()) {
+			finish();
+		}
+	}
+	// end password dialog callbacks
+
+	@Override
+	protected void onActivityResult(int requestCode, int resultCode, Intent resultData) {
+		if (resultCode != RESULT_OK) {
+			if (requestCode != REQUEST_ID_DISABLE_BATTERY_OPTIMIZATIONS && requestCode != ThreemaActivity.ACTIVITY_ID_BACKUP_PICKER) {
+				if (safeMDMConfig.isRestoreDisabled()) {
+					finish();
+				}
+			}
+		}
+
+		switch (requestCode) {
+			case REQUEST_ID_DISABLE_BATTERY_OPTIMIZATIONS:
+				FileUtil.selectFile(WizardBackupRestoreActivity.this, null, new String[]{MimeUtil.MIME_TYPE_ZIP}, ThreemaActivity.ACTIVITY_ID_BACKUP_PICKER, false, 0, fileService.getBackupPath().getPath());
+				break;
+
+			case ThreemaActivity.ACTIVITY_ID_RESTORE_KEY:
+				if (resultCode == RESULT_OK) {
+					setResult(RESULT_OK);
+					startNextWizard();
+				}
+				break;
+
+			case ThreemaActivity.ACTIVITY_ID_BACKUP_PICKER:
+				if (resultCode == RESULT_OK) {
+					setResult(RESULT_OK);
+					if (resultData != null) {
+						Uri uri;
+
+						uri = resultData.getData();
+						if (uri != null) {
+							restoreBackup(uri);
+						}
+					}
+				}
+				break;
+		}
+		super.onActivityResult(requestCode, resultCode, resultData);
+	}
+
+	private void startNextWizard() {
+		if (this.userService.hasIdentity()) {
+			this.preferenceService.setWizardRunning(true);
+			startActivity(new Intent(this, WizardBaseActivity.class));
+			overridePendingTransition(R.anim.abc_fade_in, R.anim.abc_fade_out);
+			finish();
+		}
+	}
+}

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

@@ -86,8 +86,8 @@ import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.TextUtil;
 import ch.threema.app.utils.TextUtil;
 import ch.threema.app.workers.IdentityStatesWorker;
 import ch.threema.app.workers.IdentityStatesWorker;
-import ch.threema.client.LinkEmailException;
-import ch.threema.client.LinkMobileNoException;
+import ch.threema.domain.protocol.api.LinkEmailException;
+import ch.threema.domain.protocol.api.LinkMobileNoException;
 import ch.threema.localcrypto.MasterKeyLockedException;
 import ch.threema.localcrypto.MasterKeyLockedException;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.ContactModel;
 
 
@@ -159,14 +159,14 @@ public class WizardBaseActivity extends ThreemaAppCompatActivity implements View
 					if (safeConfig.isBackupForced()) {
 					if (safeConfig.isBackupForced()) {
 						setPage(WizardFragment1.PAGE_ID);
 						setPage(WizardFragment1.PAGE_ID);
 					} else if (!isReadOnlyProfile()) {
 					} else if (!isReadOnlyProfile()) {
-						WizardDialog wizardDialog = WizardDialog.newInstance(R.string.safe_disable_confirm, R.string.yes, R.string.no);
+						WizardDialog wizardDialog = WizardDialog.newInstance(R.string.safe_disable_confirm, R.string.yes, R.string.no, WizardDialog.Highlight.NEGATIVE);
 						wizardDialog.show(getSupportFragmentManager(), DIALOG_TAG_THREEMA_SAFE);
 						wizardDialog.show(getSupportFragmentManager(), DIALOG_TAG_THREEMA_SAFE);
 					}
 					}
 				}
 				}
 
 
 				if (current == WizardFragment3.PAGE_ID && previous == WizardFragment2.PAGE_ID && TestUtil.empty(nickname)) {
 				if (current == WizardFragment3.PAGE_ID && previous == WizardFragment2.PAGE_ID && TestUtil.empty(nickname)) {
 					if (!isReadOnlyProfile()) {
 					if (!isReadOnlyProfile()) {
-						WizardDialog wizardDialog = WizardDialog.newInstance(R.string.new_wizard_use_id_as_nickname, R.string.yes, R.string.no);
+						WizardDialog wizardDialog = WizardDialog.newInstance(R.string.new_wizard_use_id_as_nickname, R.string.yes, R.string.no, WizardDialog.Highlight.POSITIVE);
 						wizardDialog.show(getSupportFragmentManager(), DIALOG_TAG_USE_ID_AS_NICKNAME);
 						wizardDialog.show(getSupportFragmentManager(), DIALOG_TAG_USE_ID_AS_NICKNAME);
 					}
 					}
 				}
 				}
@@ -197,7 +197,7 @@ public class WizardBaseActivity extends ThreemaAppCompatActivity implements View
 									ConfigUtils.isWorkBuild() ?
 									ConfigUtils.isWorkBuild() ?
 											R.string.new_wizard_anonymous_confirm :
 											R.string.new_wizard_anonymous_confirm :
 											R.string.new_wizard_anonymous_confirm_phone_only,
 											R.string.new_wizard_anonymous_confirm_phone_only,
-									R.string.yes, R.string.no);
+									R.string.yes, R.string.no, WizardDialog.Highlight.NEGATIVE);
 							wizardDialog.show(getSupportFragmentManager(), DIALOG_TAG_USE_ANONYMOUSLY);
 							wizardDialog.show(getSupportFragmentManager(), DIALOG_TAG_USE_ANONYMOUSLY);
 						}
 						}
 					}
 					}
@@ -396,7 +396,7 @@ public class WizardBaseActivity extends ThreemaAppCompatActivity implements View
 								WizardDialog wizardDialog = WizardDialog.newInstance(AppRestrictionUtil.getSafePasswordMessage(context), R.string.try_again);
 								WizardDialog wizardDialog = WizardDialog.newInstance(AppRestrictionUtil.getSafePasswordMessage(context), R.string.try_again);
 								wizardDialog.show(getSupportFragmentManager(), DIALOG_TAG_PASSWORD_BAD);
 								wizardDialog.show(getSupportFragmentManager(), DIALOG_TAG_PASSWORD_BAD);
 							} else {
 							} else {
-								WizardDialog wizardDialog = WizardDialog.newInstance(R.string.password_bad_explain, R.string.try_again, R.string.continue_anyway);
+								WizardDialog wizardDialog = WizardDialog.newInstance(R.string.password_bad_explain, R.string.continue_anyway, R.string.try_again, WizardDialog.Highlight.NEGATIVE);
 								wizardDialog.show(getSupportFragmentManager(), DIALOG_TAG_PASSWORD_BAD);
 								wizardDialog.show(getSupportFragmentManager(), DIALOG_TAG_PASSWORD_BAD);
 							}
 							}
 						}
 						}
@@ -592,7 +592,6 @@ public class WizardBaseActivity extends ThreemaAppCompatActivity implements View
 				prevPage();
 				prevPage();
 				break;
 				break;
 			case DIALOG_TAG_PASSWORD_BAD:
 			case DIALOG_TAG_PASSWORD_BAD:
-				setPage(WizardFragment1.PAGE_ID);
 				break;
 				break;
 			case DIALOG_TAG_THREEMA_SAFE:
 			case DIALOG_TAG_THREEMA_SAFE:
 				break;
 				break;
@@ -612,6 +611,7 @@ public class WizardBaseActivity extends ThreemaAppCompatActivity implements View
 				prevPage();
 				prevPage();
 				break;
 				break;
 			case DIALOG_TAG_PASSWORD_BAD:
 			case DIALOG_TAG_PASSWORD_BAD:
+				setPage(WizardFragment1.PAGE_ID);
 				break;
 				break;
 		}
 		}
 	}
 	}

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

@@ -92,6 +92,8 @@ public class WizardFingerPrintActivity extends WizardBackgroundActivity implemen
 						}
 						}
 					}
 					}
 				}, PROGRESS_MAX);
 				}, PROGRESS_MAX);
+
+		findViewById(R.id.cancel).setOnClickListener(v -> finish());
 	}
 	}
 
 
 	@SuppressLint("StaticFieldLeak")
 	@SuppressLint("StaticFieldLeak")

+ 7 - 5
app/src/main/java/ch/threema/app/activities/wizard/WizardRestoreIDActivity.java → app/src/main/java/ch/threema/app/activities/wizard/WizardIDRestoreActivity.java

@@ -43,7 +43,6 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.slf4j.LoggerFactory;
 
 
 import androidx.annotation.NonNull;
 import androidx.annotation.NonNull;
-import ch.threema.app.utils.QRScannerUtil;
 import ch.threema.app.R;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.dialogs.GenericProgressDialog;
 import ch.threema.app.dialogs.GenericProgressDialog;
@@ -51,10 +50,11 @@ import ch.threema.app.dialogs.SimpleStringAlertDialog;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.DialogUtil;
 import ch.threema.app.utils.DialogUtil;
 import ch.threema.app.utils.EditTextUtil;
 import ch.threema.app.utils.EditTextUtil;
-import ch.threema.client.ThreemaConnection;
+import ch.threema.app.utils.QRScannerUtil;
+import ch.threema.domain.protocol.csp.connection.ThreemaConnection;
 
 
-public class WizardRestoreIDActivity extends WizardBackgroundActivity {
-	private static final Logger logger = LoggerFactory.getLogger(WizardRestoreIDActivity.class);
+public class WizardIDRestoreActivity extends WizardBackgroundActivity {
+	private static final Logger logger = LoggerFactory.getLogger(WizardIDRestoreActivity.class);
 	private static final String DIALOG_TAG_RESTORE_PROGRESS = "rp";
 	private static final String DIALOG_TAG_RESTORE_PROGRESS = "rp";
 	private static final int PERMISSION_REQUEST_CAMERA = 1;
 	private static final int PERMISSION_REQUEST_CAMERA = 1;
 
 
@@ -114,7 +114,7 @@ public class WizardRestoreIDActivity extends WizardBackgroundActivity {
 		findViewById(R.id.wizard_finish).setOnClickListener(this::restoreID);
 		findViewById(R.id.wizard_finish).setOnClickListener(this::restoreID);
 		findViewById(R.id.wizard_cancel).setOnClickListener(this::onCancel);
 		findViewById(R.id.wizard_cancel).setOnClickListener(this::onCancel);
 		findViewById(R.id.wizard_scan).setOnClickListener(v -> {
 		findViewById(R.id.wizard_scan).setOnClickListener(v -> {
-			if (ConfigUtils.requestCameraPermissions(WizardRestoreIDActivity.this, null, PERMISSION_REQUEST_CAMERA)) {
+			if (ConfigUtils.requestCameraPermissions(WizardIDRestoreActivity.this, null, PERMISSION_REQUEST_CAMERA)) {
 				scanQR();
 				scanQR();
 			}
 			}
 		});
 		});
@@ -197,6 +197,7 @@ public class WizardRestoreIDActivity extends WizardBackgroundActivity {
 				logger.error(getString(R.string.invalid_barcode), this);
 				logger.error(getString(R.string.invalid_barcode), this);
 			}
 			}
 		}
 		}
+		super.onActivityResult(requestCode, resultCode, intent);
 	}
 	}
 
 
 	@Override
 	@Override
@@ -207,6 +208,7 @@ public class WizardRestoreIDActivity extends WizardBackgroundActivity {
 	@TargetApi(Build.VERSION_CODES.M)
 	@TargetApi(Build.VERSION_CODES.M)
 	@Override
 	@Override
 	public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
 	public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+		super.onRequestPermissionsResult(requestCode, permissions, grantResults);
 		switch (requestCode) {
 		switch (requestCode) {
 			case PERMISSION_REQUEST_CAMERA:
 			case PERMISSION_REQUEST_CAMERA:
 				if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
 				if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

+ 18 - 83
app/src/main/java/ch/threema/app/activities/wizard/WizardIntroActivity.java

@@ -30,73 +30,42 @@ import android.text.TextUtils;
 import android.text.method.LinkMovementMethod;
 import android.text.method.LinkMovementMethod;
 import android.text.style.ClickableSpan;
 import android.text.style.ClickableSpan;
 import android.view.View;
 import android.view.View;
-import android.widget.CompoundButton;
 import android.widget.ImageView;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 import android.widget.TextView;
 
 
-import java.util.Date;
-
-import androidx.appcompat.widget.AppCompatCheckBox;
 import ch.threema.app.R;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.activities.PrivacyPolicyActivity;
 import ch.threema.app.activities.PrivacyPolicyActivity;
-import ch.threema.app.dialogs.WizardDialog;
-import ch.threema.app.services.PreferenceService;
 import ch.threema.app.threemasafe.ThreemaSafeMDMConfig;
 import ch.threema.app.threemasafe.ThreemaSafeMDMConfig;
 import ch.threema.app.utils.AnimationUtil;
 import ch.threema.app.utils.AnimationUtil;
 import ch.threema.app.utils.AppRestrictionUtil;
 import ch.threema.app.utils.AppRestrictionUtil;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.TestUtil;
 
 
-public class WizardIntroActivity extends WizardBackgroundActivity implements WizardDialog.WizardDialogCallback {
-
-	private static final String DIALOG_TAG_CHECK_PP = "pp";
+public class WizardIntroActivity extends WizardBackgroundActivity {
 	private static final int ACTIVITY_RESULT_PRIVACY_POLICY = 9442;
 	private static final int ACTIVITY_RESULT_PRIVACY_POLICY = 9442;
 
 
 	private AnimationDrawable frameAnimation;
 	private AnimationDrawable frameAnimation;
-	private AppCompatCheckBox privacyPolicyCheckBox;
-	private LinearLayout buttonLayout;
 
 
 	@Override
 	@Override
 	protected void onCreate(Bundle savedInstanceState) {
 	protected void onCreate(Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
 		super.onCreate(savedInstanceState);
 		setContentView(R.layout.activity_wizard_intro);
 		setContentView(R.layout.activity_wizard_intro);
 
 
-		privacyPolicyCheckBox = findViewById(R.id.wizard_switch_accept_privacy_policy);
-		privacyPolicyCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
-			@Override
-			public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
-				if (!isChecked) {
-					if (preferenceService.getPrivacyPolicyAccepted() != null) {
-						preferenceService.clearPrivacyPolicyAccepted();
-					}
-				} else {
-					privacyPolicyCheckBox.setBackgroundDrawable(getResources().getDrawable(R.drawable.shape_switch));
-				}
-			}
-		});
-		TextView privacyPolicyExplainText = findViewById(R.id.wizard_privacy_policy_explain);
-		buttonLayout = findViewById(R.id.button_layout);
-
 		if (ConfigUtils.isWorkRestricted()) {
 		if (ConfigUtils.isWorkRestricted()) {
 			// Skip privacy policy check if admin pre-set a backup to restore - either Safe or ID
 			// Skip privacy policy check if admin pre-set a backup to restore - either Safe or ID
 			if (ThreemaSafeMDMConfig.getInstance().isRestoreForced()) {
 			if (ThreemaSafeMDMConfig.getInstance().isRestoreForced()) {
-				checkPrivacyPolicy(true);
-				restoreBackup(null);
+				startActivity(new Intent(this, WizardSafeRestoreActivity.class));
 				finish();
 				finish();
 				return;
 				return;
 			} else {
 			} else {
 				String backupString = AppRestrictionUtil.getStringRestriction(getString(R.string.restriction__id_backup));
 				String backupString = AppRestrictionUtil.getStringRestriction(getString(R.string.restriction__id_backup));
 				String backupPassword = AppRestrictionUtil.getStringRestriction(getString(R.string.restriction__id_backup_password));
 				String backupPassword = AppRestrictionUtil.getStringRestriction(getString(R.string.restriction__id_backup_password));
 				if (!TestUtil.empty(backupString) && !TestUtil.empty(backupPassword)) {
 				if (!TestUtil.empty(backupString) && !TestUtil.empty(backupPassword)) {
-					checkPrivacyPolicy(true);
-					Intent intent = new Intent(this, WizardRestoreMainActivity.class);
-
-					if (!TestUtil.empty(backupString) && !TestUtil.empty(backupPassword)) {
-						intent.putExtra(ThreemaApplication.INTENT_DATA_ID_BACKUP, backupString);
-						intent.putExtra(ThreemaApplication.INTENT_DATA_ID_BACKUP_PW, backupPassword);
-					}
+					Intent intent = new Intent(this, WizardBackupRestoreActivity.class);
+					intent.putExtra(ThreemaApplication.INTENT_DATA_ID_BACKUP, backupString);
+					intent.putExtra(ThreemaApplication.INTENT_DATA_ID_BACKUP_PW, backupPassword);
 					startActivity(intent);
 					startActivity(intent);
 					overridePendingTransition(R.anim.abc_fade_in, R.anim.abc_fade_out);
 					overridePendingTransition(R.anim.abc_fade_in, R.anim.abc_fade_out);
 					finish();
 					finish();
@@ -105,6 +74,7 @@ public class WizardIntroActivity extends WizardBackgroundActivity implements Wiz
 			}
 			}
 		}
 		}
 
 
+		LinearLayout buttonLayout = findViewById(R.id.button_layout);
 		if (savedInstanceState == null) {
 		if (savedInstanceState == null) {
 			buttonLayout.setVisibility(View.GONE);
 			buttonLayout.setVisibility(View.GONE);
 			buttonLayout.postDelayed(() -> AnimationUtil.slideInFromBottomOvershoot(buttonLayout), 200);
 			buttonLayout.postDelayed(() -> AnimationUtil.slideInFromBottomOvershoot(buttonLayout), 200);
@@ -116,8 +86,8 @@ public class WizardIntroActivity extends WizardBackgroundActivity implements Wiz
 		frameAnimation.setOneShot(false);
 		frameAnimation.setOneShot(false);
 		frameAnimation.start();
 		frameAnimation.start();
 
 
-		if (preferenceService.getPrivacyPolicyAccepted() != null) {
-			privacyPolicyCheckBox.setVisibility(View.GONE);
+		TextView privacyPolicyExplainText = findViewById(R.id.wizard_privacy_policy_explain);
+		if (TestUtil.empty(ThreemaApplication.getAppContext().getString(R.string.privacy_policy_url))) {
 			privacyPolicyExplainText.setVisibility(View.GONE);
 			privacyPolicyExplainText.setVisibility(View.GONE);
 		} else {
 		} else {
 			String privacyPolicy = getString(R.string.privacy_policy);
 			String privacyPolicy = getString(R.string.privacy_policy);
@@ -135,19 +105,20 @@ public class WizardIntroActivity extends WizardBackgroundActivity implements Wiz
 			privacyPolicyExplainText.setMovementMethod(LinkMovementMethod.getInstance());
 			privacyPolicyExplainText.setMovementMethod(LinkMovementMethod.getInstance());
 		}
 		}
 
 
+		((TextView) findViewById(R.id.new_to_threema_title)).setText(getString(R.string.new_to_threema, getString(R.string.app_name)));
+		((TextView) findViewById(R.id.back_to_threema_title)).setText(getString(R.string.back_to_threema, getString(R.string.app_name)));
+
 		findViewById(R.id.restore_backup).setOnClickListener(this::restoreBackup);
 		findViewById(R.id.restore_backup).setOnClickListener(this::restoreBackup);
 		findViewById(R.id.setup_threema).setOnClickListener(this::setupThreema);
 		findViewById(R.id.setup_threema).setOnClickListener(this::setupThreema);
 	}
 	}
 
 
 	public void setupThreema(View view) {
 	public void setupThreema(View view) {
-		if (checkPrivacyPolicy(false)) {
-			if (!userService.hasIdentity()) {
-				startActivity(new Intent(this, WizardFingerPrintActivity.class));
-			} else {
-				startActivity(new Intent(this, WizardBaseActivity.class));
-			}
-			overridePendingTransition(R.anim.abc_fade_in, R.anim.abc_fade_out);
+		if (!userService.hasIdentity()) {
+			startActivity(new Intent(this, WizardFingerPrintActivity.class));
+		} else {
+			startActivity(new Intent(this, WizardBaseActivity.class));
 		}
 		}
+		overridePendingTransition(R.anim.abc_fade_in, R.anim.abc_fade_out);
 	}
 	}
 
 
 	/**
 	/**
@@ -155,24 +126,8 @@ public class WizardIntroActivity extends WizardBackgroundActivity implements Wiz
 	 * @param view
 	 * @param view
 	 */
 	 */
 	public void restoreBackup(View view) {
 	public void restoreBackup(View view) {
-		if (checkPrivacyPolicy(false)) {
-			startActivity(new Intent(this, WizardRestoreMainActivity.class));
-			overridePendingTransition(R.anim.abc_fade_in, R.anim.abc_fade_out);
-		}
-	}
-
-	private boolean checkPrivacyPolicy(boolean force) {
-		if (preferenceService.getPrivacyPolicyAccepted() != null) {
-			return true;
-		}
-
-		if (!privacyPolicyCheckBox.isChecked() && !force) {
-			WizardDialog.newInstance(String.format(getString(R.string.privacy_policy_check_confirm), getString(R.string.app_name)), R.string.ok).show(getSupportFragmentManager(), DIALOG_TAG_CHECK_PP);
-			return false;
-		}
-
-		preferenceService.setPrivacyPolicyAccepted(new Date(), force ? PreferenceService.PRIVACY_POLICY_ACCEPT_IMPLICIT : PreferenceService.PRIVACY_POLICY_ACCEPT_EXCPLICIT);
-		return true;
+		startActivity(new Intent(this, WizardBackupRestoreActivity.class));
+		overridePendingTransition(R.anim.abc_fade_in, R.anim.abc_fade_out);
 	}
 	}
 
 
 	@Override
 	@Override
@@ -186,26 +141,6 @@ public class WizardIntroActivity extends WizardBackgroundActivity implements Wiz
 		}
 		}
 	}
 	}
 
 
-	@Override
-	public void onYes(String tag, Object data) {
-		privacyPolicyCheckBox.setBackgroundDrawable(getResources().getDrawable(R.drawable.shape_switch_alert));
-		privacyPolicyCheckBox.postDelayed(() -> {
-			if (!isFinishing()) {
-				privacyPolicyCheckBox.setBackgroundDrawable(getResources().getDrawable(R.drawable.shape_switch));
-			}
-		}, 400);
-		privacyPolicyCheckBox.postDelayed(() -> {
-			if (!isFinishing()) {
-				privacyPolicyCheckBox.setBackgroundDrawable(getResources().getDrawable(R.drawable.shape_switch_alert));
-			}
-		}, 600);
-	}
-
-	@Override
-	public void onNo(String tag) {
-
-	}
-
 	@Override
 	@Override
 	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
 	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
 		super.onActivityResult(requestCode, resultCode, data);
 		super.onActivityResult(requestCode, resultCode, data);

+ 32 - 262
app/src/main/java/ch/threema/app/activities/wizard/WizardRestoreMainActivity.java → app/src/main/java/ch/threema/app/activities/wizard/WizardSafeRestoreActivity.java

@@ -22,12 +22,6 @@
 package ch.threema.app.activities.wizard;
 package ch.threema.app.activities.wizard;
 
 
 import android.annotation.SuppressLint;
 import android.annotation.SuppressLint;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.AsyncTask;
 import android.os.Build;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Bundle;
@@ -36,28 +30,19 @@ import android.text.InputType;
 import android.view.View;
 import android.view.View;
 import android.widget.Button;
 import android.widget.Button;
 import android.widget.EditText;
 import android.widget.EditText;
-import android.widget.TextView;
 import android.widget.Toast;
 import android.widget.Toast;
 
 
 import org.slf4j.Logger;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.slf4j.LoggerFactory;
 
 
-import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.IOException;
 
 
-import androidx.annotation.UiThread;
-import androidx.core.content.ContextCompat;
 import ch.threema.app.R;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.ThreemaApplication;
-import ch.threema.app.activities.DisableBatteryOptimizationsActivity;
-import ch.threema.app.activities.ThreemaActivity;
-import ch.threema.app.backuprestore.csv.RestoreService;
-import ch.threema.app.dialogs.GenericAlertDialog;
 import ch.threema.app.dialogs.GenericProgressDialog;
 import ch.threema.app.dialogs.GenericProgressDialog;
 import ch.threema.app.dialogs.PasswordEntryDialog;
 import ch.threema.app.dialogs.PasswordEntryDialog;
 import ch.threema.app.dialogs.SimpleStringAlertDialog;
 import ch.threema.app.dialogs.SimpleStringAlertDialog;
-import ch.threema.app.dialogs.WizardRestoreSelectorDialog;
 import ch.threema.app.dialogs.WizardSafeSearchPhoneDialog;
 import ch.threema.app.dialogs.WizardSafeSearchPhoneDialog;
 import ch.threema.app.threemasafe.ThreemaSafeAdvancedDialog;
 import ch.threema.app.threemasafe.ThreemaSafeAdvancedDialog;
 import ch.threema.app.threemasafe.ThreemaSafeMDMConfig;
 import ch.threema.app.threemasafe.ThreemaSafeMDMConfig;
@@ -66,27 +51,20 @@ import ch.threema.app.threemasafe.ThreemaSafeService;
 import ch.threema.app.threemasafe.ThreemaSafeServiceImpl;
 import ch.threema.app.threemasafe.ThreemaSafeServiceImpl;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.DialogUtil;
 import ch.threema.app.utils.DialogUtil;
-import ch.threema.app.utils.FileUtil;
-import ch.threema.app.utils.MimeUtil;
-import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.base.ThreemaException;
 import ch.threema.base.ThreemaException;
-import ch.threema.client.ProtocolDefines;
+import ch.threema.domain.protocol.csp.ProtocolDefines;
 
 
-public class WizardRestoreMainActivity extends WizardBackgroundActivity implements GenericAlertDialog.DialogClickListener, PasswordEntryDialog.PasswordEntryDialogClickListener, WizardSafeSearchPhoneDialog.WizardSafeSearchPhoneDialogCallback,  WizardRestoreSelectorDialog.WizardRestoreSelectorDialogCallback, ThreemaSafeAdvancedDialog.WizardDialogCallback {
-	private static final Logger logger = LoggerFactory.getLogger(WizardRestoreMainActivity.class);
+public class WizardSafeRestoreActivity extends WizardBackgroundActivity implements PasswordEntryDialog.PasswordEntryDialogClickListener,
+	WizardSafeSearchPhoneDialog.WizardSafeSearchPhoneDialogCallback,
+	ThreemaSafeAdvancedDialog.WizardDialogCallback {
+	private static final Logger logger = LoggerFactory.getLogger(WizardSafeRestoreActivity.class);
 
 
 	private static final String DIALOG_TAG_PASSWORD = "tpw";
 	private static final String DIALOG_TAG_PASSWORD = "tpw";
 	private static final String DIALOG_TAG_PROGRESS = "tpr";
 	private static final String DIALOG_TAG_PROGRESS = "tpr";
 	private static final String DIALOG_TAG_FORGOT_ID = "li";
 	private static final String DIALOG_TAG_FORGOT_ID = "li";
-	private static final String DIALOG_TAG_RESTORE_SELECTOR = "rss";
-	private static final String DIALOG_TAG_DISABLE_ENERGYSAVE_CONFIRM = "de";
-	private static final String DIALOG_TAG_DOWNLOADING_BACKUP = "dwnldBkp";
-	private static final String DIALOG_TAG_NO_INTERNET = "nin";
 	private static final String DIALOG_TAG_ADVANCED = "adv";
 	private static final String DIALOG_TAG_ADVANCED = "adv";
 
 
-	public static final int REQUEST_ID_DISABLE_BATTERY_OPTIMIZATIONS = 541;
-
 	private ThreemaSafeService threemaSafeService;
 	private ThreemaSafeService threemaSafeService;
 
 
 	EditText identityEditText;
 	EditText identityEditText;
@@ -97,7 +75,7 @@ public class WizardRestoreMainActivity extends WizardBackgroundActivity implemen
 	public void onCreate(Bundle savedInstanceState) {
 	public void onCreate(Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
 		super.onCreate(savedInstanceState);
 
 
-		setContentView(R.layout.activity_wizard_restore_main);
+		setContentView(R.layout.activity_wizard_restore_safe);
 
 
 		try {
 		try {
 			threemaSafeService = ThreemaApplication.getServiceManager().getThreemaSafeService();
 			threemaSafeService = ThreemaApplication.getServiceManager().getThreemaSafeService();
@@ -115,7 +93,6 @@ public class WizardRestoreMainActivity extends WizardBackgroundActivity implemen
 				this.identityEditText.setText(safeMDMConfig.getIdentity());
 				this.identityEditText.setText(safeMDMConfig.getIdentity());
 				this.identityEditText.setEnabled(false);
 				this.identityEditText.setEnabled(false);
 
 
-				findViewById(R.id.backup_restore_other_button).setVisibility(View.GONE);
 				findViewById(R.id.safe_restore_subtitle).setVisibility(View.INVISIBLE);
 				findViewById(R.id.safe_restore_subtitle).setVisibility(View.INVISIBLE);
 				findViewById(R.id.forgot_id).setVisibility(View.GONE);
 				findViewById(R.id.forgot_id).setVisibility(View.GONE);
 
 
@@ -135,10 +112,10 @@ public class WizardRestoreMainActivity extends WizardBackgroundActivity implemen
 			}
 			}
 		});
 		});
 
 
-		findViewById(R.id.backup_restore_other_button).setOnClickListener(new View.OnClickListener() {
+		findViewById(R.id.cancel).setOnClickListener(new View.OnClickListener() {
 			@Override
 			@Override
 			public void onClick(View v) {
 			public void onClick(View v) {
-				showOtherRestoreOptions();
+				finish();
 			}
 			}
 		});
 		});
 
 
@@ -167,27 +144,25 @@ public class WizardRestoreMainActivity extends WizardBackgroundActivity implemen
 				advancedOptions.setEnabled(false);
 				advancedOptions.setEnabled(false);
 				advancedOptions.setVisibility(View.GONE);
 				advancedOptions.setVisibility(View.GONE);
 			}
 			}
-			Intent intent = getIntent();
-			if (intent.hasExtra(ThreemaApplication.INTENT_DATA_ID_BACKUP) &&
-				intent.hasExtra(ThreemaApplication.INTENT_DATA_ID_BACKUP_PW)) {
-
-				launchIdRecovery(intent.getStringExtra(ThreemaApplication.INTENT_DATA_ID_BACKUP),
-					intent.getStringExtra(ThreemaApplication.INTENT_DATA_ID_BACKUP_PW));
-			}
-			if (safeMDMConfig.isRestoreDisabled()) {
-				findViewById(R.id.safe_restore_button).setVisibility(View.GONE);
-				((TextView) findViewById(R.id.safe_restore_title)).setText(R.string.restore);
-				findViewById(R.id.safe_restore_subtitle).setVisibility(View.INVISIBLE);
-				findViewById(R.id.forgot_id).setVisibility(View.GONE);
-				identityEditText.setVisibility(View.GONE);
-				advancedOptions.setVisibility(View.GONE);
-				showOtherRestoreOptions();
-			}
 		}
 		}
 	}
 	}
 
 
-	private void showOtherRestoreOptions() {
-		WizardRestoreSelectorDialog.newInstance().show(getSupportFragmentManager(), DIALOG_TAG_RESTORE_SELECTOR);
+	@Override
+	protected void onPause() {
+		ThreemaApplication.activityPaused(this);
+		super.onPause();
+	}
+
+	@Override
+	protected void onResume() {
+		ThreemaApplication.activityResumed(this);
+		super.onResume();
+	}
+
+	@Override
+	public void onUserInteraction() {
+		ThreemaApplication.activityUserInteract(this);
+		super.onUserInteraction();
 	}
 	}
 
 
 	private void doSafeRestore() {
 	private void doSafeRestore() {
@@ -198,7 +173,8 @@ public class WizardRestoreMainActivity extends WizardBackgroundActivity implemen
 			R.string.ok,
 			R.string.ok,
 			R.string.cancel,
 			R.string.cancel,
 			ThreemaSafeServiceImpl.MIN_PW_LENGTH,
 			ThreemaSafeServiceImpl.MIN_PW_LENGTH,
-			ThreemaSafeServiceImpl.MAX_PW_LENGTH, 0, 0, 0);
+			ThreemaSafeServiceImpl.MAX_PW_LENGTH,
+			0, 0, 0, PasswordEntryDialog.ForgotHintType.SAFE);
 		dialogFragment.show(getSupportFragmentManager(), DIALOG_TAG_PASSWORD);
 		dialogFragment.show(getSupportFragmentManager(), DIALOG_TAG_PASSWORD);
 	}
 	}
 
 
@@ -284,7 +260,7 @@ public class WizardRestoreMainActivity extends WizardBackgroundActivity implemen
 					}
 					}
 					ConfigUtils.scheduleAppRestart(getApplicationContext(), 3000, getApplicationContext().getString(R.string.ipv6_restart_now));
 					ConfigUtils.scheduleAppRestart(getApplicationContext(), 3000, getApplicationContext().getString(R.string.ipv6_restart_now));
 				} else {
 				} else {
-					Toast.makeText(WizardRestoreMainActivity.this, getString(R.string.safe_restore_failed) + ". " + failureMessage, Toast.LENGTH_LONG).show();
+					Toast.makeText(WizardSafeRestoreActivity.this, getString(R.string.safe_restore_failed) + ". " + failureMessage, Toast.LENGTH_LONG).show();
 					if (safeMDMConfig.isRestoreForced()) {
 					if (safeMDMConfig.isRestoreForced()) {
 						finish();
 						finish();
 					}
 					}
@@ -293,165 +269,6 @@ public class WizardRestoreMainActivity extends WizardBackgroundActivity implemen
 		}.execute();
 		}.execute();
 	}
 	}
 
 
-	private void launchIdRecovery(String backupString, String backupPassword) {
-		Intent intent = new Intent(this, WizardRestoreIDActivity.class);
-
-		if (!TestUtil.empty(backupString) && !TestUtil.empty(backupPassword)) {
-			intent.putExtra(ThreemaApplication.INTENT_DATA_ID_BACKUP, backupString);
-			intent.putExtra(ThreemaApplication.INTENT_DATA_ID_BACKUP_PW, backupPassword);
-		}
-		startActivityForResult(intent, ThreemaActivity.ACTIVITY_ID_RESTORE_KEY);
-		overridePendingTransition(R.anim.abc_fade_in, R.anim.abc_fade_out);
-	}
-
-	private void showDisableEnergySaveDialog() {
-		GenericAlertDialog.newInstance(R.string.menu_restore, R.string.restore_disable_energy_saving, R.string.ok, R.string.cancel).show(getSupportFragmentManager(), DIALOG_TAG_DISABLE_ENERGYSAVE_CONFIRM);
-	}
-
-	private void doDataBackupRestore(final Uri uri) {
-		if (!ContentResolver.SCHEME_FILE.equalsIgnoreCase(uri.getScheme()) && this.fileService != null) {
-			// copy "file" to cache directory first
-			GenericProgressDialog.newInstance(R.string.importing_files, R.string.please_wait).show(getSupportFragmentManager(), DIALOG_TAG_DOWNLOADING_BACKUP);
-
-			new Thread(() -> {
-				final File file = fileService.copyUriToTempFile(uri, "file", "zip", true);
-
-				RuntimeUtil.runOnUiThread(() -> {
-					DialogUtil.dismissDialog(getSupportFragmentManager(), DIALOG_TAG_DOWNLOADING_BACKUP, true);
-
-					if (file != null) {
-						performDataBackupRestore(file);
-					} else {
-						SimpleStringAlertDialog.newInstance(R.string.importing_files, R.string.importing_files_failed).show(getSupportFragmentManager(), "ifail");
-					}
-				});
-			}).start();
-
-		} else {
-			String path = FileUtil.getRealPathFromURI(this, uri);
-			if (path != null && !path.isEmpty()) {
-				File file = new File(path);
-				if (file.exists()) {
-					performDataBackupRestore(file);
-				}
-			}
-		}
-	}
-
-	private void performDataBackupRestore(File file) {
-		if (file.exists()) {
-//			try {
-// Zipfile validity check is sometimes wrong
-//				ZipFile zipFile = new ZipFile(file);
-//				if (zipFile.isValidZipFile()) {
-				ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
-
-				NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
-				if (activeNetwork == null || !activeNetwork.isConnectedOrConnecting()) {
-					showNoInternetDialog(file);
-				} else {
-					confirmRestore(file);
-				}
-				return;
-			}
-//				}
-//			} catch (ZipException e) {
-//				logger.error("Exception", e);
-//			}
-		logger.error(getString(R.string.invalid_backup), this);
-	}
-
-	private void confirmRestore(File file) {
-		PasswordEntryDialog dialogFragment = PasswordEntryDialog.newInstance(
-			R.string.backup_data_title,
-			R.string.restore_data_password_msg,
-			R.string.password_hint,
-			R.string.ok,
-			R.string.cancel,
-			ThreemaApplication.MIN_PW_LENGTH_BACKUP,
-			ThreemaApplication.MAX_PW_LENGTH_BACKUP, 0, 0, 0);
-		dialogFragment.setData(file);
-		dialogFragment.show(getSupportFragmentManager(), "restorePW");
-	}
-
-	private void startNextWizard() {
-		logger.debug("start next wizard");
-
-		if (this.userService.hasIdentity()) {
-			serviceManager.getPreferenceService().setWizardRunning(true);
-
-			startActivity(new Intent(this, WizardBaseActivity.class));
-			overridePendingTransition(R.anim.abc_fade_in, R.anim.abc_fade_out);
-			finish();
-		}
-	}
-
-	@UiThread
-	private void showNoInternetDialog(File file) {
-		GenericAlertDialog dialog = GenericAlertDialog.newInstance(R.string.menu_restore, R.string.new_wizard_need_internet, R.string.retry, R.string.cancel);
-		dialog.setData(file);
-		dialog.show(getSupportFragmentManager(), DIALOG_TAG_NO_INTERNET);
-	}
-
-	@Override
-	protected void onPause() {
-		ThreemaApplication.activityPaused(this);
-		super.onPause();
-	}
-
-	@Override
-	protected void onResume() {
-		ThreemaApplication.activityResumed(this);
-		super.onResume();
-	}
-
-	@Override
-	public void onUserInteraction() {
-		ThreemaApplication.activityUserInteract(this);
-		super.onUserInteraction();
-	}
-
-	@Override
-	protected void onActivityResult(int requestCode, int resultCode, Intent resultData) {
-		logger.debug("Restore: {} {}", requestCode, resultCode);
-
-		if (resultCode != RESULT_OK) {
-			if (requestCode != REQUEST_ID_DISABLE_BATTERY_OPTIMIZATIONS && requestCode != ThreemaActivity.ACTIVITY_ID_BACKUP_PICKER) {
-				if (safeMDMConfig.isRestoreDisabled()) {
-					finish();
-				}
-			}
-		}
-
-		switch (requestCode) {
-			case REQUEST_ID_DISABLE_BATTERY_OPTIMIZATIONS:
-				FileUtil.selectFile(WizardRestoreMainActivity.this, null, new String[]{MimeUtil.MIME_TYPE_ZIP}, ThreemaActivity.ACTIVITY_ID_BACKUP_PICKER, false, 0, fileService.getBackupPath().getPath());
-				break;
-
-			case ThreemaActivity.ACTIVITY_ID_RESTORE_KEY:
-				if (resultCode == RESULT_OK) {
-					setResult(RESULT_OK);
-					startNextWizard();
-				}
-				break;
-
-			case ThreemaActivity.ACTIVITY_ID_BACKUP_PICKER:
-				if (resultCode == RESULT_OK) {
-					setResult(RESULT_OK);
-					if (resultData != null) {
-						Uri uri;
-
-						uri = resultData.getData();
-						if (uri != null) {
-							doDataBackupRestore(uri);
-						}
-					}
-				}
-				break;
-		}
-		super.onActivityResult(requestCode, resultCode, resultData);
-	}
-
 	@Override
 	@Override
 	public void onBackPressed() {
 	public void onBackPressed() {
 		finish();
 		finish();
@@ -459,19 +276,9 @@ public class WizardRestoreMainActivity extends WizardBackgroundActivity implemen
 
 
 	@Override
 	@Override
 	public void onYes(String tag, String text, boolean isChecked, Object data) {
 	public void onYes(String tag, String text, boolean isChecked, Object data) {
-		if (DIALOG_TAG_PASSWORD.equals(tag)) {
-			// safe backup restore
-			if (!TestUtil.empty(text)) {
-				reallySafeRestore(text);
-			}
-		} else {
-			// data backup restore
-			Intent intent = new Intent(this, RestoreService.class);
-			intent.putExtra(RestoreService.EXTRA_RESTORE_BACKUP_FILE, (File) data);
-			intent.putExtra(RestoreService.EXTRA_RESTORE_BACKUP_PASSWORD, text);
-			ContextCompat.startForegroundService(this, intent);
-
-			finish();
+		// safe backup restore
+		if (!TestUtil.empty(text)) {
+			reallySafeRestore(text);
 		}
 		}
 	}
 	}
 
 
@@ -484,21 +291,6 @@ public class WizardRestoreMainActivity extends WizardBackgroundActivity implemen
 		}
 		}
 	}
 	}
 
 
-	@Override
-	public void onYes(String tag, Object data) {
-		switch (tag) {
-			case DIALOG_TAG_DISABLE_ENERGYSAVE_CONFIRM:
-				Intent intent = new Intent(this, DisableBatteryOptimizationsActivity.class);
-				intent.putExtra(DisableBatteryOptimizationsActivity.EXTRA_NAME, getString(R.string.restore));
-				intent.putExtra(DisableBatteryOptimizationsActivity.EXTRA_WIZARD, true);
-				startActivityForResult(intent, REQUEST_ID_DISABLE_BATTERY_OPTIMIZATIONS);
-				break;
-			case DIALOG_TAG_NO_INTERNET:
-				performDataBackupRestore((File) data);
-				break;
-		}
-	}
-
 	@Override
 	@Override
 	public void onYes(String tag, ThreemaSafeServerInfo serverInfo) {
 	public void onYes(String tag, ThreemaSafeServerInfo serverInfo) {
 		this.serverInfo = serverInfo;
 		this.serverInfo = serverInfo;
@@ -506,30 +298,8 @@ public class WizardRestoreMainActivity extends WizardBackgroundActivity implemen
 
 
 	@Override
 	@Override
 	public void onNo(String tag) {
 	public void onNo(String tag) {
-		if (DIALOG_TAG_RESTORE_SELECTOR.equals(tag)) {
-			if (safeMDMConfig.isRestoreDisabled()) {
-				finish();
-			}
-		}
-	}
-
-	@Override
-	public void onNo(String tag, Object data) {
-		if (DIALOG_TAG_DISABLE_ENERGYSAVE_CONFIRM.equals(tag)) {
-			if (safeMDMConfig.isRestoreDisabled()) {
-				finish();
-			}
+		if (safeMDMConfig.isRestoreDisabled()) {
+			finish();
 		}
 		}
 	}
 	}
-
-	@Override
-	public void onDataBackupRestore() {
-		showDisableEnergySaveDialog();
-	}
-
-	@Override
-	public void onIdBackupRestore() {
-		launchIdRecovery(null, null);
-	}
-
 }
 }

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

@@ -80,7 +80,7 @@ import ch.threema.app.utils.MimeUtil;
 import ch.threema.app.utils.NameUtil;
 import ch.threema.app.utils.NameUtil;
 import ch.threema.app.utils.QuoteUtil;
 import ch.threema.app.utils.QuoteUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.TestUtil;
-import ch.threema.client.file.FileData;
+import ch.threema.domain.protocol.csp.messages.file.FileData;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.DateSeparatorMessageModel;
 import ch.threema.storage.models.DateSeparatorMessageModel;

+ 12 - 10
app/src/main/java/ch/threema/app/adapters/ContactDetailAdapter.java

@@ -42,12 +42,13 @@ import org.slf4j.LoggerFactory;
 import java.util.List;
 import java.util.List;
 
 
 import androidx.annotation.NonNull;
 import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.recyclerview.widget.RecyclerView;
 import ch.threema.app.R;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.ThreemaApplication;
+import ch.threema.app.dialogs.PublicKeyDialog;
 import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.services.ContactService;
 import ch.threema.app.services.ContactService;
-import ch.threema.app.services.FingerPrintService;
 import ch.threema.app.services.GroupService;
 import ch.threema.app.services.GroupService;
 import ch.threema.app.services.IdListService;
 import ch.threema.app.services.IdListService;
 import ch.threema.app.services.PreferenceService;
 import ch.threema.app.services.PreferenceService;
@@ -67,7 +68,6 @@ public class ContactDetailAdapter extends RecyclerView.Adapter<RecyclerView.View
 	private ContactService contactService;
 	private ContactService contactService;
 	private GroupService groupService;
 	private GroupService groupService;
 	private PreferenceService preferenceService;
 	private PreferenceService preferenceService;
-	private FingerPrintService fingerprintService;
 	private IdListService excludeFromSyncListService;
 	private IdListService excludeFromSyncListService;
 	private IdListService blackListIdentityService;
 	private IdListService blackListIdentityService;
 	private final ContactModel contactModel;
 	private final ContactModel contactModel;
@@ -90,7 +90,7 @@ public class ContactDetailAdapter extends RecyclerView.Adapter<RecyclerView.View
 
 
 	public class HeaderHolder extends RecyclerView.ViewHolder {
 	public class HeaderHolder extends RecyclerView.ViewHolder {
 		private final VerificationLevelImageView verificationLevelImageView;
 		private final VerificationLevelImageView verificationLevelImageView;
-		private final TextView threemaIdView, fingerprintView;
+		private final TextView threemaIdView;
 		private final CheckBox synchronize;
 		private final CheckBox synchronize;
 		private final View nicknameContainer, synchronizeContainer;
 		private final View nicknameContainer, synchronizeContainer;
 		private final ImageView syncSourceIcon;
 		private final ImageView syncSourceIcon;
@@ -101,9 +101,7 @@ public class ContactDetailAdapter extends RecyclerView.Adapter<RecyclerView.View
 		public HeaderHolder(View view) {
 		public HeaderHolder(View view) {
 			super(view);
 			super(view);
 
 
-			// items in object
 			this.threemaIdView = itemView.findViewById(R.id.threema_id);
 			this.threemaIdView = itemView.findViewById(R.id.threema_id);
-			this.fingerprintView = itemView.findViewById(R.id.key_fingerprint);
 			this.verificationLevelImageView = itemView.findViewById(R.id.verification_level_image);
 			this.verificationLevelImageView = itemView.findViewById(R.id.verification_level_image);
 			ImageView verificationLevelIconView = itemView.findViewById(R.id.verification_information_icon);
 			ImageView verificationLevelIconView = itemView.findViewById(R.id.verification_information_icon);
 			this.synchronize = itemView.findViewById(R.id.synchronize_contact);
 			this.synchronize = itemView.findViewById(R.id.synchronize_contact);
@@ -123,6 +121,13 @@ public class ContactDetailAdapter extends RecyclerView.Adapter<RecyclerView.View
 					}
 					}
 				}
 				}
 			});
 			});
+
+			itemView.findViewById(R.id.public_key_button).setOnClickListener(v -> {
+				if (context instanceof AppCompatActivity) {
+					PublicKeyDialog.newInstance(context.getString(R.string.public_key_for, contactModel.getIdentity()), contactModel.getPublicKey())
+						.show(((AppCompatActivity) context).getSupportFragmentManager(), "pk");
+				}
+			});
 		}
 		}
 	}
 	}
 
 
@@ -135,7 +140,6 @@ public class ContactDetailAdapter extends RecyclerView.Adapter<RecyclerView.View
 		try {
 		try {
 			this.contactService = serviceManager.getContactService();
 			this.contactService = serviceManager.getContactService();
 			this.groupService = serviceManager.getGroupService();
 			this.groupService = serviceManager.getGroupService();
-			this.fingerprintService = serviceManager.getFingerPrintService();
 			this.excludeFromSyncListService = serviceManager.getExcludedSyncIdentitiesService();
 			this.excludeFromSyncListService = serviceManager.getExcludedSyncIdentitiesService();
 			this.blackListIdentityService = serviceManager.getBlackListService();
 			this.blackListIdentityService = serviceManager.getBlackListService();
 			this.preferenceService = serviceManager.getPreferenceService();
 			this.preferenceService = serviceManager.getPreferenceService();
@@ -182,7 +186,6 @@ public class ContactDetailAdapter extends RecyclerView.Adapter<RecyclerView.View
 			String identityAdditional = null;
 			String identityAdditional = null;
 			if(this.contactModel.getState() != null) {
 			if(this.contactModel.getState() != null) {
 				switch (this.contactModel.getState()) {
 				switch (this.contactModel.getState()) {
-					case TEMPORARY:
 					case ACTIVE:
 					case ACTIVE:
 						if (blackListIdentityService.has(contactModel.getIdentity())) {
 						if (blackListIdentityService.has(contactModel.getIdentity())) {
 							identityAdditional = context.getString(R.string.blocked);
 							identityAdditional = context.getString(R.string.blocked);
@@ -197,7 +200,6 @@ public class ContactDetailAdapter extends RecyclerView.Adapter<RecyclerView.View
 				}
 				}
 			}
 			}
 			headerHolder.threemaIdView.setText(contactModel.getIdentity() + (identityAdditional != null ? " (" + identityAdditional + ")" : ""));
 			headerHolder.threemaIdView.setText(contactModel.getIdentity() + (identityAdditional != null ? " (" + identityAdditional + ")" : ""));
-			headerHolder.fingerprintView.setText(this.fingerprintService.getFingerPrint(contactModel.getIdentity()));
 			headerHolder.verificationLevelImageView.setContactModel(contactModel);
 			headerHolder.verificationLevelImageView.setContactModel(contactModel);
 			headerHolder.verificationLevelImageView.setVisibility(View.VISIBLE);
 			headerHolder.verificationLevelImageView.setVisibility(View.VISIBLE);
 
 
@@ -271,9 +273,9 @@ public class ContactDetailAdapter extends RecyclerView.Adapter<RecyclerView.View
 
 
 	@Override
 	@Override
 	public int getItemViewType(int position) {
 	public int getItemViewType(int position) {
-		if (isPositionHeader(position))
+		if (isPositionHeader(position)) {
 			return TYPE_HEADER;
 			return TYPE_HEADER;
-
+		}
 		return TYPE_ITEM;
 		return TYPE_ITEM;
 	}
 	}
 
 

+ 52 - 9
app/src/main/java/ch/threema/app/adapters/ContactListAdapter.java

@@ -30,9 +30,13 @@ import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewGroup;
 import android.widget.Filter;
 import android.widget.Filter;
 import android.widget.ImageView;
 import android.widget.ImageView;
+import android.widget.ListView;
 import android.widget.SectionIndexer;
 import android.widget.SectionIndexer;
 import android.widget.TextView;
 import android.widget.TextView;
 
 
+import com.google.android.material.card.MaterialCardView;
+import com.google.android.material.shape.ShapeAppearanceModel;
+
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.NotNull;
 import org.slf4j.Logger;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.slf4j.LoggerFactory;
@@ -48,6 +52,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.List;
 
 
 import androidx.annotation.NonNull;
 import androidx.annotation.NonNull;
+import androidx.constraintlayout.widget.ConstraintLayout;
 import ch.threema.app.R;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.emojis.EmojiTextView;
 import ch.threema.app.emojis.EmojiTextView;
@@ -75,13 +80,14 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
 	private final IdListService blackListIdentityService;
 	private final IdListService blackListIdentityService;
 
 
 	public static final int VIEW_TYPE_NORMAL = 0;
 	public static final int VIEW_TYPE_NORMAL = 0;
+	public static final int VIEW_TYPE_RECENTLY_ADDED = 1;
 	public static final int VIEW_TYPE_COUNT = 2;
 	public static final int VIEW_TYPE_COUNT = 2;
 
 
 	private static final String PLACEHOLDER_BLANK_HEADER = " ";
 	private static final String PLACEHOLDER_BLANK_HEADER = " ";
 	private static final String PLACEHOLDER_CHANNELS = "\uffff";
 	private static final String PLACEHOLDER_CHANNELS = "\uffff";
 	private static final String PLACEHOLDER_RECENTLY_ADDED = "\u0001";
 	private static final String PLACEHOLDER_RECENTLY_ADDED = "\u0001";
 	private static final String CHANNEL_SIGN = "\u002a";
 	private static final String CHANNEL_SIGN = "\u002a";
-	private static final String RECENTLY_ADDED_SIGN = "+";
+	public static final String RECENTLY_ADDED_SIGN = "+";
 
 
 	private List<ContactModel> values, ovalues, recentlyAdded = new ArrayList<>();
 	private List<ContactModel> values, ovalues, recentlyAdded = new ArrayList<>();
 	private ContactListFilter contactListFilter;
 	private ContactListFilter contactListFilter;
@@ -240,7 +246,7 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
 		} else {
 		} else {
 			if (ContactUtil.isChannelContact(c)) {
 			if (ContactUtil.isChannelContact(c)) {
 				firstLetter = afterSorting ? CHANNEL_SIGN : PLACEHOLDER_CHANNELS;
 				firstLetter = afterSorting ? CHANNEL_SIGN : PLACEHOLDER_CHANNELS;
-			} else if (recentlyAdded != null && recentlyAdded.size() > 0 && position < recentlyAdded.size() && recentlyAdded.contains(c)) {
+			} else if (getItemViewType(position) == VIEW_TYPE_RECENTLY_ADDED) {
 				if (contactListFilter != null && contactListFilter.getFilterString() != null) {
 				if (contactListFilter != null && contactListFilter.getFilterString() != null) {
 					if (position > 0) {
 					if (position > 0) {
 						for (int i = Math.min(position - 1, MAX_RECENTLY_ADDED_CONTACTS - 1) ; i >= 0; i--) {
 						for (int i = Math.min(position - 1, MAX_RECENTLY_ADDED_CONTACTS - 1) ; i >= 0; i--) {
@@ -272,14 +278,19 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
 	@NonNull
 	@NonNull
 	@Override
 	@Override
 	public View getView(final int position, View convertView, @NonNull ViewGroup parent) {
 	public View getView(final int position, View convertView, @NonNull ViewGroup parent) {
-		CheckableConstraintLayout itemView = (CheckableConstraintLayout) convertView;
+		final int viewType = getItemViewType(position);
+		ConstraintLayout itemView = (ConstraintLayout) convertView;
 
 
 		ContactListHolder holder;
 		ContactListHolder holder;
 
 
 		if (convertView == null) {
 		if (convertView == null) {
 			// This a new view we inflate the new layout
 			// This a new view we inflate the new layout
 			holder = new ContactListHolder();
 			holder = new ContactListHolder();
-			itemView = (CheckableConstraintLayout) inflater.inflate(R.layout.item_contact_list, parent, false);
+
+			itemView = (ConstraintLayout) inflater.inflate(
+						viewType == VIEW_TYPE_RECENTLY_ADDED ?
+						R.layout.item_contact_list_recently_added :
+						R.layout.item_contact_list, parent, false);
 
 
 			holder.nameTextView = itemView.findViewById(R.id.name);
 			holder.nameTextView = itemView.findViewById(R.id.name);
 			holder.idTextView = itemView.findViewById(R.id.subject);
 			holder.idTextView = itemView.findViewById(R.id.subject);
@@ -291,16 +302,29 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
 			holder.initialImageView = itemView.findViewById(R.id.initial_image);
 			holder.initialImageView = itemView.findViewById(R.id.initial_image);
 
 
 			itemView.setTag(holder);
 			itemView.setTag(holder);
-			itemView.setOnCheckedChangeListener(new CheckableConstraintLayout.OnCheckedChangeListener() {
-				@Override
-				public void onCheckedChanged(CheckableConstraintLayout checkableView, boolean isChecked) {
+
+			if (viewType == VIEW_TYPE_NORMAL) {
+				((CheckableConstraintLayout) itemView).setOnCheckedChangeListener((checkableView, isChecked) -> {
 					if (isChecked) {
 					if (isChecked) {
 						checkedItems.add(((ContactListHolder) checkableView.getTag()).originalPosition);
 						checkedItems.add(((ContactListHolder) checkableView.getTag()).originalPosition);
 					} else {
 					} else {
 						checkedItems.remove(((ContactListHolder) checkableView.getTag()).originalPosition);
 						checkedItems.remove(((ContactListHolder) checkableView.getTag()).originalPosition);
 					}
 					}
-				}
-			});
+				});
+			} else if (viewType == VIEW_TYPE_RECENTLY_ADDED) {
+				int cornerSize = getContext().getResources().getDimensionPixelSize(R.dimen.recently_added_background_corner_size);
+				int recentlyAddedLastPosition = recentlyAdded.size() - 1;
+
+				ShapeAppearanceModel shapeAppearanceModel = new ShapeAppearanceModel.Builder()
+					.setTopLeftCornerSize(position == 0 ? cornerSize : 0)
+					.setTopRightCornerSize(position == 0 ? cornerSize : 0)
+					.setBottomLeftCornerSize(position == recentlyAddedLastPosition ? cornerSize : 0)
+					.setBottomRightCornerSize(position == recentlyAddedLastPosition ? cornerSize : 0)
+					.build();
+
+				MaterialCardView cardView = itemView.findViewById(R.id.recently_added_background);
+				cardView.setShapeAppearanceModel(shapeAppearanceModel);
+			}
 		} else {
 		} else {
 			holder = (ContactListHolder) itemView.getTag();
 			holder = (ContactListHolder) itemView.getTag();
 		}
 		}
@@ -386,11 +410,30 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
 
 
 		holder.avatarView.setBadgeVisible(contactService.showBadge(contactModel));
 		holder.avatarView.setBadgeVisible(contactService.showBadge(contactModel));
 
 
+		//itemView.setEnabled(viewType == VIEW_TYPE_NORMAL);
+		if (viewType == VIEW_TYPE_RECENTLY_ADDED) {
+			itemView.setOnLongClickListener(v -> true);
+			itemView.setOnClickListener(new View.OnClickListener() {
+				@Override
+				public void onClick(View v) {
+					ListView listView = (ListView) parent;
+					if (listView.getCheckedItemCount() == 0) {
+						listView.getOnItemClickListener().onItemClick(null, v, position, 0L);
+					}
+				}
+			});
+		}
+
 		return itemView;
 		return itemView;
 	}
 	}
 
 
 	@Override
 	@Override
 	public int getItemViewType(int position) {
 	public int getItemViewType(int position) {
+		ContactModel c = values.get(position);
+
+		if (recentlyAdded != null && recentlyAdded.size() > 0 && position < recentlyAdded.size() && recentlyAdded.contains(c)) {
+			return VIEW_TYPE_RECENTLY_ADDED;
+		}
 		return VIEW_TYPE_NORMAL;
 		return VIEW_TYPE_NORMAL;
 	}
 	}
 
 

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

@@ -44,9 +44,9 @@ import ch.threema.app.services.ContactService;
 import ch.threema.app.services.PreferenceService;
 import ch.threema.app.services.PreferenceService;
 import ch.threema.app.ui.InitialAvatarView;
 import ch.threema.app.ui.InitialAvatarView;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.TestUtil;
-import ch.threema.client.work.WorkDirectoryCategory;
-import ch.threema.client.work.WorkDirectoryContact;
-import ch.threema.client.work.WorkOrganization;
+import ch.threema.domain.protocol.api.work.WorkDirectoryCategory;
+import ch.threema.domain.protocol.api.work.WorkDirectoryContact;
+import ch.threema.domain.protocol.api.work.WorkOrganization;
 
 
 public class DirectoryAdapter extends PagedListAdapter<WorkDirectoryContact, RecyclerView.ViewHolder> {
 public class DirectoryAdapter extends PagedListAdapter<WorkDirectoryContact, RecyclerView.ViewHolder> {
 	private final Context context;
 	private final Context context;
@@ -56,7 +56,7 @@ public class DirectoryAdapter extends PagedListAdapter<WorkDirectoryContact, Rec
 	private final WorkOrganization workOrganization;
 	private final WorkOrganization workOrganization;
 	private final HashMap<String, String> categoryMap = new HashMap<>();
 	private final HashMap<String, String> categoryMap = new HashMap<>();
 	private DirectoryAdapter.OnClickItemListener onClickItemListener;
 	private DirectoryAdapter.OnClickItemListener onClickItemListener;
-	@DrawableRes private int backgroundRes;
+	@DrawableRes private final int backgroundRes;
 
 
 	private static class DirectoryHolder extends RecyclerView.ViewHolder {
 	private static class DirectoryHolder extends RecyclerView.ViewHolder {
 		private final TextView nameView;
 		private final TextView nameView;
@@ -113,14 +113,14 @@ public class DirectoryAdapter extends PagedListAdapter<WorkDirectoryContact, Rec
 
 
 	@NonNull
 	@NonNull
 	@Override
 	@Override
-	public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
+	public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
 		View itemView = inflater.inflate(R.layout.item_directory, viewGroup, false);
 		View itemView = inflater.inflate(R.layout.item_directory, viewGroup, false);
 		itemView.setBackgroundResource(R.drawable.listitem_background_selector);
 		itemView.setBackgroundResource(R.drawable.listitem_background_selector);
 		return new DirectoryHolder(itemView);
 		return new DirectoryHolder(itemView);
 	}
 	}
 
 
 	@Override
 	@Override
-	public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
+	public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
 		boolean isMe = false;
 		boolean isMe = false;
 		final DirectoryHolder holder = (DirectoryHolder) viewHolder;
 		final DirectoryHolder holder = (DirectoryHolder) viewHolder;
 
 

+ 121 - 20
app/src/main/java/ch/threema/app/adapters/GroupDetailAdapter.java

@@ -21,9 +21,7 @@
 
 
 package ch.threema.app.adapters;
 package ch.threema.app.adapters;
 
 
-import android.app.Activity;
 import android.content.Context;
 import android.content.Context;
-import android.content.Intent;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap;
 import android.view.LayoutInflater;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View;
@@ -34,24 +32,33 @@ import android.widget.TextView;
 import org.slf4j.Logger;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.slf4j.LoggerFactory;
 
 
+import java.io.IOException;
 import java.util.List;
 import java.util.List;
 
 
-import androidx.core.app.ActivityCompat;
-import androidx.core.app.ActivityOptionsCompat;
+import androidx.annotation.NonNull;
+import androidx.appcompat.widget.AppCompatImageButton;
+import androidx.appcompat.widget.SwitchCompat;
+import androidx.constraintlayout.widget.ConstraintLayout;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.recyclerview.widget.RecyclerView;
+import ch.threema.app.BuildConfig;
 import ch.threema.app.R;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.ThreemaApplication;
-import ch.threema.app.activities.ContactDetailActivity;
-import ch.threema.app.activities.ThreemaActivity;
+import ch.threema.app.activities.GroupDetailActivity;
+import ch.threema.app.dialogs.ShowOnceDialog;
 import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.services.ContactService;
 import ch.threema.app.services.ContactService;
+import ch.threema.app.services.group.GroupInviteService;
 import ch.threema.app.ui.AvatarView;
 import ch.threema.app.ui.AvatarView;
 import ch.threema.app.ui.SectionHeaderView;
 import ch.threema.app.ui.SectionHeaderView;
 import ch.threema.app.utils.AdapterUtil;
 import ch.threema.app.utils.AdapterUtil;
+import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.LocaleUtil;
 import ch.threema.app.utils.LocaleUtil;
 import ch.threema.app.utils.NameUtil;
 import ch.threema.app.utils.NameUtil;
+import ch.threema.domain.protocol.csp.messages.group.GroupInviteToken;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.GroupModel;
 import ch.threema.storage.models.GroupModel;
+import ch.threema.storage.models.group.GroupInviteModel;
+import java8.util.Optional;
 
 
 public class GroupDetailAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
 public class GroupDetailAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
 	private static final Logger logger = LoggerFactory.getLogger(GroupDetailAdapter.class);
 	private static final Logger logger = LoggerFactory.getLogger(GroupDetailAdapter.class);
@@ -61,9 +68,13 @@ public class GroupDetailAdapter extends RecyclerView.Adapter<RecyclerView.ViewHo
 
 
 	private Context context;
 	private Context context;
 	private ContactService contactService;
 	private ContactService contactService;
+	private GroupInviteService groupInviteService;
 	private GroupModel groupModel;
 	private GroupModel groupModel;
+	private GroupInviteModel defaultGroupInviteModel;
 	private List<ContactModel> contactModels; // Cached copy of group members
 	private List<ContactModel> contactModels; // Cached copy of group members
-	private OnClickListener onClickListener;
+	private OnGroupDetailsClickListener onClickListener;
+	HeaderHolder headerHolder;
+	private boolean warningShown = false;
 
 
 	public static class ItemHolder extends RecyclerView.ViewHolder {
 	public static class ItemHolder extends RecyclerView.ViewHolder {
 		public final View view;
 		public final View view;
@@ -86,6 +97,12 @@ public class GroupDetailAdapter extends RecyclerView.Adapter<RecyclerView.ViewHo
 		private final TextView ownerName;
 		private final TextView ownerName;
 		private final TextView ownerThreemaId;
 		private final TextView ownerThreemaId;
 		private final SectionHeaderView ownerNameTitle;
 		private final SectionHeaderView ownerNameTitle;
+		private final ConstraintLayout linkContainerView;
+		private final SectionHeaderView groupLinkTitle;
+		private final SwitchCompat linkEnableSwitch;
+		private final TextView linkString;
+		private final AppCompatImageButton linkResetButton;
+		private final AppCompatImageButton linkShareButton;
 
 
 		public HeaderHolder(View view) {
 		public HeaderHolder(View view) {
 			super(view);
 			super(view);
@@ -97,6 +114,12 @@ public class GroupDetailAdapter extends RecyclerView.Adapter<RecyclerView.ViewHo
 			this.ownerName = itemView.findViewById(R.id.group_name);
 			this.ownerName = itemView.findViewById(R.id.group_name);
 			this.ownerThreemaId = itemView.findViewById(R.id.threemaid);
 			this.ownerThreemaId = itemView.findViewById(R.id.threemaid);
 			this.ownerNameTitle = itemView.findViewById(R.id.group_owner_title);
 			this.ownerNameTitle = itemView.findViewById(R.id.group_owner_title);
+			this.linkContainerView = itemView.findViewById(R.id.group_link_container);
+			this.groupLinkTitle = itemView.findViewById(R.id.group_link_header);
+			this.linkEnableSwitch = itemView.findViewById(R.id.group_link_switch);
+			this.linkString = itemView.findViewById(R.id.group_link_string);
+			this.linkResetButton = itemView.findViewById(R.id.reset_button);
+			this.linkShareButton = itemView.findViewById(R.id.share_button);
 		}
 		}
 	}
 	}
 
 
@@ -107,8 +130,9 @@ public class GroupDetailAdapter extends RecyclerView.Adapter<RecyclerView.ViewHo
 
 
 		try {
 		try {
 			this.contactService = serviceManager.getContactService();
 			this.contactService = serviceManager.getContactService();
+			this.groupInviteService = serviceManager.getGroupInviteService();
 		} catch (Exception e) {
 		} catch (Exception e) {
-			logger.error("Exception", e);
+			logger.error("Exception, failed to get required services", e);
 		}
 		}
 	}
 	}
 
 
@@ -148,19 +172,15 @@ public class GroupDetailAdapter extends RecyclerView.Adapter<RecyclerView.ViewHo
 			itemHolder.view.setOnClickListener(new View.OnClickListener() {
 			itemHolder.view.setOnClickListener(new View.OnClickListener() {
 				@Override
 				@Override
 				public void onClick(View v) {
 				public void onClick(View v) {
-					onClickListener.onItemClick(v, contactModel);
+					onClickListener.onGroupMemberClick(v, contactModel);
 				}
 				}
 			});
 			});
 		} else {
 		} else {
-			HeaderHolder headerHolder = (HeaderHolder) holder;
+			this.headerHolder = (HeaderHolder) holder;
 			headerHolder.groupOwnerContainerView.setOnClickListener(new View.OnClickListener() {
 			headerHolder.groupOwnerContainerView.setOnClickListener(new View.OnClickListener() {
 				@Override
 				@Override
 				public void onClick(View v) {
 				public void onClick(View v) {
-					Intent intent = new Intent(context, ContactDetailActivity.class);
-					intent.putExtra(ThreemaApplication.INTENT_DATA_CONTACT, groupModel.getCreatorIdentity());
-					intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-					ActivityOptionsCompat options = ActivityOptionsCompat.makeScaleUpAnimation(v, 0, 0, v.getWidth(), v.getHeight());
-					ActivityCompat.startActivityForResult((Activity) context, intent, ThreemaActivity.ACTIVITY_ID_CONTACT_DETAIL, options.toBundle());
+					onClickListener.onGroupOwnerClick(v, groupModel.getCreatorIdentity());
 				}
 				}
 			});
 			});
 
 
@@ -171,6 +191,13 @@ public class GroupDetailAdapter extends RecyclerView.Adapter<RecyclerView.ViewHo
 				headerHolder.ownerAvatarView.setImageBitmap(bitmap);
 				headerHolder.ownerAvatarView.setImageBitmap(bitmap);
 				headerHolder.ownerThreemaId.setText(ownerContactModel.getIdentity());
 				headerHolder.ownerThreemaId.setText(ownerContactModel.getIdentity());
 				headerHolder.ownerName.setText(NameUtil.getDisplayNameOrNickname(ownerContactModel, true));
 				headerHolder.ownerName.setText(NameUtil.getDisplayNameOrNickname(ownerContactModel, true));
+
+				if (!ConfigUtils.supportsGroupLinks() || ownerContactModel != contactService.getMe()) {
+					headerHolder.linkContainerView.setVisibility(View.GONE);
+				}
+				else {
+					initGroupLinkSection();
+				}
 			} else {
 			} else {
 				// creator is no longer around / has been revoked
 				// creator is no longer around / has been revoked
 				headerHolder.ownerAvatarView.setImageBitmap(contactService.getDefaultAvatar(null, false));
 				headerHolder.ownerAvatarView.setImageBitmap(contactService.getDefaultAvatar(null, false));
@@ -183,9 +210,80 @@ public class GroupDetailAdapter extends RecyclerView.Adapter<RecyclerView.ViewHo
 
 
 			if (contactModels != null) {
 			if (contactModels != null) {
 				headerHolder.groupMembersTitleView.setText(context.getString(R.string.add_group_members_list) +
 				headerHolder.groupMembersTitleView.setText(context.getString(R.string.add_group_members_list) +
-					" (" + contactModels.size() + "/" + context.getResources().getInteger(R.integer.max_group_size) +
-					")");
+					" (" + contactModels.size() + "/" + BuildConfig.MAX_GROUP_SIZE + ")");
+			}
+		}
+	}
+
+	private void initGroupLinkSection() {
+		Optional<GroupInviteModel> groupInviteModelOptional = groupInviteService.getDefaultGroupInvite(groupModel);
+		if (groupInviteModelOptional.isPresent()) {
+			this.defaultGroupInviteModel = groupInviteModelOptional.get();
+		}
+		boolean enableGroupLinkSwitch = defaultGroupInviteModel != null && !defaultGroupInviteModel.isInvalidated();
+		headerHolder.linkEnableSwitch.setChecked(enableGroupLinkSwitch);
+		setGroupLinkViewsEnabled(enableGroupLinkSwitch);
+		if (defaultGroupInviteModel != null) {
+			encodeAndDisplayDefaultLink();
+		}
+		else {
+			headerHolder.linkString.setText(R.string.group_link_none);
+		}
+
+		headerHolder.linkEnableSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
+			GroupDetailAdapter.this.setGroupLinkViewsEnabled(isChecked);
+			if (isChecked) {
+				try {
+					GroupDetailAdapter.this.defaultGroupInviteModel = groupInviteService.createOrEnableDefaultLink(groupModel);
+					encodeAndDisplayDefaultLink();
+				} catch (GroupInviteToken.InvalidGroupInviteTokenException | IOException | GroupInviteModel.MissingRequiredArgumentsException e) {
+					logger.error("Exception, failed to create or get default group link", e);
+				}
+			}
+			else {
+				groupInviteService.deleteDefaultLink(groupModel);
+				GroupDetailAdapter.this.defaultGroupInviteModel = null;
 			}
 			}
+		});
+		headerHolder.linkShareButton.setOnClickListener(v -> onClickListener.onShareLinkClick());
+		headerHolder.linkResetButton.setOnClickListener(v -> {
+			if (!warningShown && !ShowOnceDialog.shouldNotShowAnymore(GroupDetailActivity.DIALOG_SHOW_ONCE_RESET_LINK_INFO)) {
+				// show only once dialog
+				onClickListener.onResetLinkClick();
+				warningShown = true;
+				return;
+			}
+			try {
+				this.defaultGroupInviteModel = groupInviteService.resetDefaultGroupInvite(groupModel);
+				encodeAndDisplayDefaultLink();
+			} catch (IOException | GroupInviteToken.InvalidGroupInviteTokenException | GroupInviteModel.MissingRequiredArgumentsException e) {
+				logger.error("Exception, failed to reset default group link", e);
+			}
+		});
+
+		headerHolder.groupLinkTitle.setText(context.getString(R.string.default_group_link) +
+			" (" + groupInviteService.getCustomLinksCount() + " " + context.getString(R.string.other_group_links) + ")" );
+	}
+
+	private void encodeAndDisplayDefaultLink() {
+		headerHolder.linkString.setText(
+			groupInviteService.encodeGroupInviteLink(GroupDetailAdapter.this.defaultGroupInviteModel).toString()
+		);
+	}
+
+	private void setGroupLinkViewsEnabled(boolean enabled) {
+		headerHolder.linkContainerView.setEnabled(enabled);
+		headerHolder.linkResetButton.setEnabled(enabled);
+		headerHolder.linkShareButton.setEnabled(enabled);
+		if (enabled) {
+			headerHolder.linkString.setAlpha(1F);
+			headerHolder.linkResetButton.setAlpha(1F);
+			headerHolder.linkShareButton.setAlpha(1F);
+		}
+		else {
+			headerHolder.linkString.setAlpha(0.5F);
+			headerHolder.linkResetButton.setAlpha(0.5F);
+			headerHolder.linkShareButton.setAlpha(0.5F);
 		}
 		}
 	}
 	}
 
 
@@ -216,11 +314,14 @@ public class GroupDetailAdapter extends RecyclerView.Adapter<RecyclerView.ViewHo
 		return contactModels.get(position - 1);
 		return contactModels.get(position - 1);
 	}
 	}
 
 
-	public void setOnClickListener(OnClickListener listener) {
+	public void setOnClickListener(OnGroupDetailsClickListener listener) {
 		onClickListener = listener;
 		onClickListener = listener;
 	}
 	}
 
 
-	public interface OnClickListener {
-		void onItemClick(View v, ContactModel contactModel);
+	public interface OnGroupDetailsClickListener {
+		void onGroupOwnerClick(View v, String identity);
+		void onGroupMemberClick(View v, @NonNull ContactModel contactModel);
+		void onResetLinkClick();
+		void onShareLinkClick();
 	}
 	}
 }
 }

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

@@ -502,9 +502,9 @@ public class MessageListAdapter extends AbstractRecyclerAdapter<ConversationMode
 				// handle selection in multi-pane mode
 				// handle selection in multi-pane mode
 				if (highlightUid != null && highlightUid.equals(conversationModel.getUid()) && context instanceof ComposeMessageActivity) {
 				if (highlightUid != null && highlightUid.equals(conversationModel.getUid()) && context instanceof ComposeMessageActivity) {
 					if (ConfigUtils.getAppTheme(context) == ConfigUtils.THEME_DARK) {
 					if (ConfigUtils.getAppTheme(context) == ConfigUtils.THEME_DARK) {
-						holder.listItemFG.setBackgroundResource(R.color.settings_multipane_selection_bg_dark);
+						holder.listItemFG.setBackgroundResource(R.color.dark_settings_multipane_selection_bg);
 					} else {
 					} else {
-						holder.listItemFG.setBackgroundResource(R.color.settings_multipane_selection_bg_light);
+						holder.listItemFG.setBackgroundResource(R.color.settings_multipane_selection_bg);
 					}
 					}
 				} else {
 				} else {
 					holder.listItemFG.setBackgroundColor(this.backgroundColor);
 					holder.listItemFG.setBackgroundColor(this.backgroundColor);

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

@@ -41,7 +41,7 @@ import ch.threema.app.utils.FileUtil;
 import ch.threema.app.utils.ImageViewUtil;
 import ch.threema.app.utils.ImageViewUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.TestUtil;
-import ch.threema.client.file.FileData;
+import ch.threema.domain.protocol.csp.messages.file.FileData;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.MessageState;
 import ch.threema.storage.models.MessageState;
 import ch.threema.storage.models.data.media.FileDataModel;
 import ch.threema.storage.models.data.media.FileDataModel;

+ 6 - 5
app/src/main/java/ch/threema/app/adapters/decorators/BallotChatAdapterDecorator.java

@@ -35,6 +35,7 @@ import ch.threema.app.ThreemaApplication;
 import ch.threema.app.dialogs.SelectorDialog;
 import ch.threema.app.dialogs.SelectorDialog;
 import ch.threema.app.exceptions.NotAllowedException;
 import ch.threema.app.exceptions.NotAllowedException;
 import ch.threema.app.services.GroupService;
 import ch.threema.app.services.GroupService;
+import ch.threema.app.ui.SelectorDialogItem;
 import ch.threema.app.ui.listitemholder.ComposeMessageHolder;
 import ch.threema.app.ui.listitemholder.ComposeMessageHolder;
 import ch.threema.app.utils.BallotUtil;
 import ch.threema.app.utils.BallotUtil;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.AbstractMessageModel;
@@ -135,26 +136,26 @@ public class BallotChatAdapterDecorator extends ChatAdapterDecorator {
 
 
 	private void showChooser(final BallotModel ballotModel) {
 	private void showChooser(final BallotModel ballotModel) {
 
 
-		final ArrayList<String> items = new ArrayList<>();
+		ArrayList<SelectorDialogItem> items = new ArrayList<>();
 		final ArrayList<Integer> action = new ArrayList<>();
 		final ArrayList<Integer> action = new ArrayList<>();
 		String title = null;
 		String title = null;
 
 
 		if (BallotUtil.canVote(ballotModel, helper.getMyIdentity())) {
 		if (BallotUtil.canVote(ballotModel, helper.getMyIdentity())) {
-			items.add(getContext().getString(R.string.ballot_vote));
+			items.add(new SelectorDialogItem(getContext().getString(R.string.ballot_vote), R.drawable.ic_vote_outline));
 			action.add(ACTION_VOTE);
 			action.add(ACTION_VOTE);
 		}
 		}
 
 
 		if (BallotUtil.canViewMatrix(ballotModel, helper.getMyIdentity())) {
 		if (BallotUtil.canViewMatrix(ballotModel, helper.getMyIdentity())) {
 			if (ballotModel.getState() == BallotModel.State.CLOSED) {
 			if (ballotModel.getState() == BallotModel.State.CLOSED) {
-				items.add(getContext().getString(R.string.ballot_result_final));
+				items.add(new SelectorDialogItem(getContext().getString(R.string.ballot_result_final), R.drawable.ic_ballot_outline));
 			} else {
 			} else {
-				items.add(getContext().getString(R.string.ballot_result_intermediate));
+				items.add(new SelectorDialogItem(getContext().getString(R.string.ballot_result_intermediate), R.drawable.ic_ballot_outline));
 			}
 			}
 			action.add(ACTION_RESULTS);
 			action.add(ACTION_RESULTS);
 		}
 		}
 
 
 		if (BallotUtil.canClose(ballotModel, helper.getMyIdentity())) {
 		if (BallotUtil.canClose(ballotModel, helper.getMyIdentity())) {
-			items.add(getContext().getString(R.string.ballot_close));
+			items.add(new SelectorDialogItem(getContext().getString(R.string.ballot_close), R.drawable.ic_check));
 			action.add(ACTION_CLOSE);
 			action.add(ACTION_CLOSE);
 		}
 		}
 
 

+ 6 - 13
app/src/main/java/ch/threema/app/adapters/decorators/ChatAdapterDecorator.java

@@ -22,7 +22,6 @@
 package ch.threema.app.adapters.decorators;
 package ch.threema.app.adapters.decorators;
 
 
 import android.content.Context;
 import android.content.Context;
-import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Drawable;
 import android.text.Spannable;
 import android.text.Spannable;
@@ -39,8 +38,9 @@ import org.slf4j.LoggerFactory;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map;
 
 
-import androidx.annotation.AttrRes;
+import androidx.annotation.DrawableRes;
 import androidx.annotation.Nullable;
 import androidx.annotation.Nullable;
+import androidx.appcompat.content.res.AppCompatResources;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.Fragment;
 import ch.threema.app.R;
 import ch.threema.app.R;
 import ch.threema.app.cache.ThumbnailCache;
 import ch.threema.app.cache.ThumbnailCache;
@@ -504,23 +504,16 @@ abstract public class ChatAdapterDecorator extends AdapterDecorator {
 
 
 	void setDefaultBackground(ComposeMessageHolder holder) {
 	void setDefaultBackground(ComposeMessageHolder holder) {
 		if (holder.messageBlockView.getBackground() == null) {
 		if (holder.messageBlockView.getBackground() == null) {
-			@AttrRes int attr;
+			@DrawableRes int drawableRes;
 
 
 			if (this.getMessageModel().isOutbox() && !(this.getMessageModel() instanceof DistributionListMessageModel)) {
 			if (this.getMessageModel().isOutbox() && !(this.getMessageModel() instanceof DistributionListMessageModel)) {
 				// outgoing
 				// outgoing
-				attr = R.attr.chat_bubble_send;
+				drawableRes = R.drawable.bubble_send_selector;
 			} else {
 			} else {
 				// incoming
 				// incoming
-				attr = R.attr.chat_bubble_recv;
+				drawableRes = R.drawable.bubble_recv_selector;
 			}
 			}
-
-			TypedArray typedArray;
-			typedArray = getContext().getTheme().obtainStyledAttributes(new int[] { attr });
-
-			Drawable drawable = typedArray.getDrawable(0);
-
-			typedArray.recycle();
-			holder.messageBlockView.setBackground(drawable);
+			holder.messageBlockView.setBackground(AppCompatResources.getDrawable(getContext(), drawableRes));
 
 
 			logger.debug("*** setDefaultBackground");
 			logger.debug("*** setDefaultBackground");
 		}
 		}

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

@@ -52,7 +52,7 @@ import ch.threema.app.utils.LinkifyUtil;
 import ch.threema.app.utils.MimeUtil;
 import ch.threema.app.utils.MimeUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.TestUtil;
-import ch.threema.client.file.FileData;
+import ch.threema.domain.protocol.csp.messages.file.FileData;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.DistributionListMessageModel;
 import ch.threema.storage.models.DistributionListMessageModel;
 import ch.threema.storage.models.MessageState;
 import ch.threema.storage.models.MessageState;

+ 3 - 3
app/src/main/java/ch/threema/app/adapters/decorators/TextChatAdapterDecorator.java

@@ -80,10 +80,10 @@ public class TextChatAdapterDecorator extends ChatAdapterDecorator {
 				if (messageText != null && messageText.length() > helper.getMaxBubbleTextLength()) {
 				if (messageText != null && messageText.length() > helper.getMaxBubbleTextLength()) {
 					holder.readOnContainer.setVisibility(View.VISIBLE);
 					holder.readOnContainer.setVisibility(View.VISIBLE);
 					if (quoteType != QuoteUtil.QUOTE_TYPE_NONE) {
 					if (quoteType != QuoteUtil.QUOTE_TYPE_NONE) {
-						holder.readOnContainer.setBackgroundResource(ConfigUtils.getResourceFromAttribute(getContext(),
+						holder.readOnContainer.setBackgroundResource(
 							this.getMessageModel().isOutbox() ?
 							this.getMessageModel().isOutbox() ?
-								R.attr.chat_bubble_fade_send :
-								R.attr.chat_bubble_fade_recv));
+								R.drawable.bubble_fade_send_selector :
+								R.drawable.bubble_fade_recv_selector);
 					}
 					}
 					holder.readOnButton.setOnClickListener(view -> {
 					holder.readOnButton.setOnClickListener(view -> {
 						Intent intent = new Intent(helper.getFragment().getContext(), TextChatBubbleActivity.class);
 						Intent intent = new Intent(helper.getFragment().getContext(), TextChatBubbleActivity.class);

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

@@ -28,8 +28,6 @@ import android.text.format.Formatter;
 import android.view.View;
 import android.view.View;
 import android.widget.Toast;
 import android.widget.Toast;
 
 
-import com.google.android.exoplayer2.ui.DefaultTimeBar;
-
 import org.slf4j.Logger;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.slf4j.LoggerFactory;
 
 
@@ -46,7 +44,7 @@ import ch.threema.app.utils.LinkifyUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.StringConversionUtil;
 import ch.threema.app.utils.StringConversionUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.TestUtil;
-import ch.threema.client.file.FileData;
+import ch.threema.domain.protocol.csp.messages.file.FileData;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.DistributionListMessageModel;
 import ch.threema.storage.models.DistributionListMessageModel;
 import ch.threema.storage.models.MessageState;
 import ch.threema.storage.models.MessageState;

+ 3 - 0
app/src/main/java/ch/threema/app/archive/ArchiveActivity.java

@@ -400,6 +400,9 @@ public class ArchiveActivity extends ThreemaToolbarActivity implements GenericAl
 		@Override
 		@Override
 		public void onRemoved(AbstractMessageModel removedMessageModel) {}
 		public void onRemoved(AbstractMessageModel removedMessageModel) {}
 
 
+		@Override
+		public void onRemoved(List<AbstractMessageModel> removedMessageModels) {}
+
 		@Override
 		@Override
 		public void onProgressChanged(AbstractMessageModel messageModel, int newProgress) {}
 		public void onProgressChanged(AbstractMessageModel messageModel, int newProgress) {}
 	};
 	};

+ 3 - 3
app/src/main/java/ch/threema/app/asynctasks/AddContactAsyncTask.java

@@ -31,8 +31,8 @@ import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.services.ContactService;
 import ch.threema.app.services.ContactService;
-import ch.threema.base.VerificationLevel;
-import ch.threema.client.IdentityType;
+import ch.threema.domain.models.VerificationLevel;
+import ch.threema.domain.models.IdentityType;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.ContactModel;
 
 
 public class AddContactAsyncTask extends AsyncTask<Void, Void, Boolean> {
 public class AddContactAsyncTask extends AsyncTask<Void, Void, Boolean> {
@@ -76,7 +76,7 @@ public class AddContactAsyncTask extends AsyncTask<Void, Void, Boolean> {
 					contactService.save(contactModel);
 					contactService.save(contactModel);
 				}
 				}
 
 
-				if (contactModel.getType() == IdentityType.WORK || markAsWorkVerified) {
+				if (contactModel.getIdentityType() == IdentityType.WORK || markAsWorkVerified) {
 					contactModel.setIsWork(true);
 					contactModel.setIsWork(true);
 
 
 					if(contactModel.getVerificationLevel() != VerificationLevel.FULLY_VERIFIED) {
 					if(contactModel.getVerificationLevel() != VerificationLevel.FULLY_VERIFIED) {

+ 8 - 0
app/src/main/java/ch/threema/app/asynctasks/DeleteDistributionListAsyncTask.java

@@ -27,6 +27,7 @@ import androidx.fragment.app.Fragment;
 import ch.threema.app.R;
 import ch.threema.app.R;
 import ch.threema.app.dialogs.GenericProgressDialog;
 import ch.threema.app.dialogs.GenericProgressDialog;
 import ch.threema.app.listeners.ConversationListener;
 import ch.threema.app.listeners.ConversationListener;
+import ch.threema.app.listeners.DistributionListListener;
 import ch.threema.app.managers.ListenerManager;
 import ch.threema.app.managers.ListenerManager;
 import ch.threema.app.services.DistributionListService;
 import ch.threema.app.services.DistributionListService;
 import ch.threema.app.utils.DialogUtil;
 import ch.threema.app.utils.DialogUtil;
@@ -72,6 +73,13 @@ public class DeleteDistributionListAsyncTask extends AsyncTask<Void, Void, Void>
 			}
 			}
 		});
 		});
 
 
+		ListenerManager.distributionListListeners.handle(new ListenerManager.HandleListener<DistributionListListener>() {
+			@Override
+			public void handle(DistributionListListener listener) {
+				listener.onRemove(distributionListModel);
+			}
+		});
+
 		if (runOnCompletion != null) {
 		if (runOnCompletion != null) {
 			runOnCompletion.run();
 			runOnCompletion.run();
 		}
 		}

+ 1 - 1
app/src/main/java/ch/threema/app/asynctasks/DeleteIdentityAsyncTask.java

@@ -41,7 +41,7 @@ import ch.threema.app.utils.DialogUtil;
 import ch.threema.app.utils.SecureDeleteUtil;
 import ch.threema.app.utils.SecureDeleteUtil;
 import ch.threema.app.webclient.services.SessionWakeUpServiceImpl;
 import ch.threema.app.webclient.services.SessionWakeUpServiceImpl;
 import ch.threema.app.webclient.services.instance.DisconnectContext;
 import ch.threema.app.webclient.services.instance.DisconnectContext;
-import ch.threema.client.ThreemaConnection;
+import ch.threema.domain.protocol.csp.connection.ThreemaConnection;
 import ch.threema.storage.DatabaseServiceNew;
 import ch.threema.storage.DatabaseServiceNew;
 import ch.threema.storage.NonceDatabaseBlobService;
 import ch.threema.storage.NonceDatabaseBlobService;
 
 

+ 8 - 0
app/src/main/java/ch/threema/app/asynctasks/DeleteMyGroupAsyncTask.java

@@ -28,6 +28,7 @@ import androidx.appcompat.app.AppCompatActivity;
 import ch.threema.app.R;
 import ch.threema.app.R;
 import ch.threema.app.dialogs.GenericProgressDialog;
 import ch.threema.app.dialogs.GenericProgressDialog;
 import ch.threema.app.listeners.ConversationListener;
 import ch.threema.app.listeners.ConversationListener;
+import ch.threema.app.listeners.GroupListener;
 import ch.threema.app.managers.ListenerManager;
 import ch.threema.app.managers.ListenerManager;
 import ch.threema.app.services.GroupService;
 import ch.threema.app.services.GroupService;
 import ch.threema.app.utils.DialogUtil;
 import ch.threema.app.utils.DialogUtil;
@@ -77,6 +78,13 @@ public class DeleteMyGroupAsyncTask extends AsyncTask<Void, Void, Void> {
 			}
 			}
 		});
 		});
 
 
+		ListenerManager.groupListeners.handle(new ListenerManager.HandleListener<GroupListener>() {
+			@Override
+			public void handle(GroupListener listener) {
+				listener.onRemove(groupModel);
+			}
+		});
+
 		if (runOnCompletion != null) {
 		if (runOnCompletion != null) {
 			runOnCompletion.run();
 			runOnCompletion.run();
 		}
 		}

+ 56 - 12
app/src/main/java/ch/threema/app/asynctasks/EmptyChatAsyncTask.java

@@ -23,31 +23,48 @@ package ch.threema.app.asynctasks;
 
 
 import android.os.AsyncTask;
 import android.os.AsyncTask;
 
 
+import net.sqlcipher.database.SQLiteException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
 import androidx.fragment.app.FragmentManager;
 import androidx.fragment.app.FragmentManager;
 import ch.threema.app.R;
 import ch.threema.app.R;
-import ch.threema.app.dialogs.GenericProgressDialog;
+import ch.threema.app.dialogs.CancelableHorizontalProgressDialog;
+import ch.threema.app.managers.ListenerManager;
 import ch.threema.app.messagereceiver.MessageReceiver;
 import ch.threema.app.messagereceiver.MessageReceiver;
+import ch.threema.app.services.ConversationService;
 import ch.threema.app.services.MessageService;
 import ch.threema.app.services.MessageService;
 import ch.threema.app.utils.DialogUtil;
 import ch.threema.app.utils.DialogUtil;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.AbstractMessageModel;
 
 
-public class EmptyChatAsyncTask extends AsyncTask<Void, Void, Integer> {
+public class EmptyChatAsyncTask extends AsyncTask<Void, Integer, Integer> {
+	private static final Logger logger = LoggerFactory.getLogger(EmptyChatAsyncTask.class);
+
 	private static final String DIALOG_TAG_EMPTYING_CHAT = "ec";
 	private static final String DIALOG_TAG_EMPTYING_CHAT = "ec";
 
 
-	private final MessageReceiver[] messageReceivers;
+	private final MessageReceiver messageReceiver;
 	private final MessageService messageService;
 	private final MessageService messageService;
+	private final ConversationService conversationService;
 	private final FragmentManager fragmentManager;
 	private final FragmentManager fragmentManager;
 	private final boolean quiet;
 	private final boolean quiet;
 	private final Runnable runOnCompletion;
 	private final Runnable runOnCompletion;
+	private int progress;
+	private boolean isCancelled;
 
 
-	public EmptyChatAsyncTask(MessageReceiver[] messageReceivers,
+	public EmptyChatAsyncTask(MessageReceiver messageReceiver,
 	                          MessageService messageService,
 	                          MessageService messageService,
+	                          ConversationService conversationService,
 	                          FragmentManager fragmentManager,
 	                          FragmentManager fragmentManager,
 	                          boolean quiet,
 	                          boolean quiet,
 	                          Runnable runOnCompletion) {
 	                          Runnable runOnCompletion) {
 
 
-		this.messageReceivers = messageReceivers;
+		this.messageReceiver = messageReceiver;
 		this.messageService = messageService;
 		this.messageService = messageService;
+		this.conversationService = conversationService;
 		this.fragmentManager = fragmentManager;
 		this.fragmentManager = fragmentManager;
 		this.quiet = quiet;
 		this.quiet = quiet;
 		this.runOnCompletion = runOnCompletion;
 		this.runOnCompletion = runOnCompletion;
@@ -56,20 +73,47 @@ public class EmptyChatAsyncTask extends AsyncTask<Void, Void, Integer> {
 	@Override
 	@Override
 	protected void onPreExecute() {
 	protected void onPreExecute() {
 		if (!quiet) {
 		if (!quiet) {
-			GenericProgressDialog.newInstance(R.string.emptying_chat, R.string.please_wait).show(fragmentManager, DIALOG_TAG_EMPTYING_CHAT);
+			isCancelled = false;
+			CancelableHorizontalProgressDialog dialog = CancelableHorizontalProgressDialog.newInstance(R.string.emptying_chat, 0, R.string.cancel, 100);
+			dialog.setOnCancelListener((dialog1, which) -> isCancelled = true);
+			dialog.show(fragmentManager, DIALOG_TAG_EMPTYING_CHAT);
+		}
+	}
+
+	@Override
+	protected void onProgressUpdate(Integer... values) {
+		if (quiet) {
+			return;
+		}
+		if (values[0] > progress) {
+			DialogUtil.updateProgress(fragmentManager, DIALOG_TAG_EMPTYING_CHAT, values[0]);
+			progress = values[0];
 		}
 		}
 	}
 	}
 
 
 	@Override
 	@Override
 	protected Integer doInBackground(Void... voids) {
 	protected Integer doInBackground(Void... voids) {
-		int i = 0;
-		for (MessageReceiver messageReceiver: messageReceivers) {
-			for (AbstractMessageModel m : messageService.getMessagesForReceiver(messageReceiver)) {
-				messageService.remove(m, true);
-				i++;
+		List<AbstractMessageModel> messageModelsToDelete = new ArrayList<>(messageService.getMessagesForReceiver(messageReceiver));
+		int i = 0, size = messageModelsToDelete.size();
+		if (size > 0) {
+			for (AbstractMessageModel abstractMessageModel : messageModelsToDelete) {
+				if (isCancelled) {
+					break;
+				}
+
+				publishProgress(i++ * 100 / size);
+				try {
+					messageService.remove(abstractMessageModel, true);
+				} catch (SQLiteException e) {
+					logger.error("Unable to remove message", e);
+				}
 			}
 			}
+
+			ListenerManager.messageListeners.handle(listener -> listener.onRemoved(messageModelsToDelete));
+
+			conversationService.refresh(messageReceiver);
 		}
 		}
-		return i;
+		return size;
 	}
 	}
 
 
 	@Override
 	@Override

+ 8 - 0
app/src/main/java/ch/threema/app/asynctasks/LeaveGroupAsyncTask.java

@@ -28,6 +28,7 @@ import androidx.appcompat.app.AppCompatActivity;
 import ch.threema.app.R;
 import ch.threema.app.R;
 import ch.threema.app.dialogs.GenericProgressDialog;
 import ch.threema.app.dialogs.GenericProgressDialog;
 import ch.threema.app.listeners.ConversationListener;
 import ch.threema.app.listeners.ConversationListener;
+import ch.threema.app.listeners.GroupListener;
 import ch.threema.app.managers.ListenerManager;
 import ch.threema.app.managers.ListenerManager;
 import ch.threema.app.services.GroupService;
 import ch.threema.app.services.GroupService;
 import ch.threema.app.utils.DialogUtil;
 import ch.threema.app.utils.DialogUtil;
@@ -76,6 +77,13 @@ public class LeaveGroupAsyncTask extends AsyncTask<Void, Void, Void> {
 			}
 			}
 		});
 		});
 
 
+		ListenerManager.groupListeners.handle(new ListenerManager.HandleListener<GroupListener>() {
+			@Override
+			public void handle(GroupListener listener) {
+				listener.onLeave(groupModel);
+			}
+		});
+
 		if (runOnCompletion != null) {
 		if (runOnCompletion != null) {
 			runOnCompletion.run();
 			runOnCompletion.run();
 		}
 		}

+ 17 - 5
app/src/main/java/ch/threema/app/backuprestore/csv/BackupService.java

@@ -85,8 +85,8 @@ import ch.threema.app.utils.StringConversionUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.ZipUtil;
 import ch.threema.app.utils.ZipUtil;
 import ch.threema.base.ThreemaException;
 import ch.threema.base.ThreemaException;
-import ch.threema.client.IdentityBackupGenerator;
-import ch.threema.client.Utils;
+import ch.threema.base.utils.Utils;
+import ch.threema.domain.identitybackup.IdentityBackupGenerator;
 import ch.threema.storage.DatabaseServiceNew;
 import ch.threema.storage.DatabaseServiceNew;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.ContactModel;
@@ -538,7 +538,9 @@ public class BackupService extends Service {
 			Tags.TAG_MESSAGE_IS_STATUS_MESSAGE,
 			Tags.TAG_MESSAGE_IS_STATUS_MESSAGE,
 			Tags.TAG_MESSAGE_IS_QUEUED,
 			Tags.TAG_MESSAGE_IS_QUEUED,
 			Tags.TAG_MESSAGE_CAPTION,
 			Tags.TAG_MESSAGE_CAPTION,
-			Tags.TAG_MESSAGE_QUOTED_MESSAGE_ID
+			Tags.TAG_MESSAGE_QUOTED_MESSAGE_ID,
+			Tags.TAG_MESSAGE_DELIVERED_AT,
+			Tags.TAG_MESSAGE_READ_AT
 		};
 		};
 
 
 		// Iterate over all contacts. Then backup every contact with the corresponding messages.
 		// Iterate over all contacts. Then backup every contact with the corresponding messages.
@@ -624,6 +626,8 @@ public class BackupService extends Service {
 										.write(Tags.TAG_MESSAGE_IS_QUEUED, messageModel.isQueued())
 										.write(Tags.TAG_MESSAGE_IS_QUEUED, messageModel.isQueued())
 										.write(Tags.TAG_MESSAGE_CAPTION, messageModel.getCaption())
 										.write(Tags.TAG_MESSAGE_CAPTION, messageModel.getCaption())
 										.write(Tags.TAG_MESSAGE_QUOTED_MESSAGE_ID, messageModel.getQuotedMessageId())
 										.write(Tags.TAG_MESSAGE_QUOTED_MESSAGE_ID, messageModel.getQuotedMessageId())
+										.write(Tags.TAG_MESSAGE_DELIVERED_AT, messageModel.getDeliveredAt())
+										.write(Tags.TAG_MESSAGE_READ_AT, messageModel.getReadAt())
 										.write();
 										.write();
 								}
 								}
 
 
@@ -689,7 +693,9 @@ public class BackupService extends Service {
 			Tags.TAG_MESSAGE_IS_STATUS_MESSAGE,
 			Tags.TAG_MESSAGE_IS_STATUS_MESSAGE,
 			Tags.TAG_MESSAGE_IS_QUEUED,
 			Tags.TAG_MESSAGE_IS_QUEUED,
 			Tags.TAG_MESSAGE_CAPTION,
 			Tags.TAG_MESSAGE_CAPTION,
-			Tags.TAG_MESSAGE_QUOTED_MESSAGE_ID
+			Tags.TAG_MESSAGE_QUOTED_MESSAGE_ID,
+			Tags.TAG_MESSAGE_DELIVERED_AT,
+			Tags.TAG_MESSAGE_READ_AT
 		};
 		};
 
 
 		final GroupService.GroupFilter groupFilter = new GroupService.GroupFilter() {
 		final GroupService.GroupFilter groupFilter = new GroupService.GroupFilter() {
@@ -778,6 +784,8 @@ public class BackupService extends Service {
 									.write(Tags.TAG_MESSAGE_IS_QUEUED, groupMessageModel.isQueued())
 									.write(Tags.TAG_MESSAGE_IS_QUEUED, groupMessageModel.isQueued())
 									.write(Tags.TAG_MESSAGE_CAPTION, groupMessageModel.getCaption())
 									.write(Tags.TAG_MESSAGE_CAPTION, groupMessageModel.getCaption())
 									.write(Tags.TAG_MESSAGE_QUOTED_MESSAGE_ID, groupMessageModel.getQuotedMessageId())
 									.write(Tags.TAG_MESSAGE_QUOTED_MESSAGE_ID, groupMessageModel.getQuotedMessageId())
+									.write(Tags.TAG_MESSAGE_DELIVERED_AT, groupMessageModel.getDeliveredAt())
+									.write(Tags.TAG_MESSAGE_READ_AT, groupMessageModel.getReadAt())
 									.write();
 									.write();
 
 
 								if (MessageUtil.hasDataFile(groupMessageModel)) {
 								if (MessageUtil.hasDataFile(groupMessageModel)) {
@@ -1032,7 +1040,9 @@ public class BackupService extends Service {
 			Tags.TAG_MESSAGE_IS_STATUS_MESSAGE,
 			Tags.TAG_MESSAGE_IS_STATUS_MESSAGE,
 			Tags.TAG_MESSAGE_IS_QUEUED,
 			Tags.TAG_MESSAGE_IS_QUEUED,
 			Tags.TAG_MESSAGE_CAPTION,
 			Tags.TAG_MESSAGE_CAPTION,
-			Tags.TAG_MESSAGE_QUOTED_MESSAGE_ID
+			Tags.TAG_MESSAGE_QUOTED_MESSAGE_ID,
+			Tags.TAG_MESSAGE_DELIVERED_AT,
+			Tags.TAG_MESSAGE_READ_AT
 		};
 		};
 
 
 		try (final ByteArrayOutputStream distributionListBuffer = new ByteArrayOutputStream()) {
 		try (final ByteArrayOutputStream distributionListBuffer = new ByteArrayOutputStream()) {
@@ -1080,6 +1090,8 @@ public class BackupService extends Service {
 										.write(Tags.TAG_MESSAGE_IS_QUEUED, distributionListMessageModel.isQueued())
 										.write(Tags.TAG_MESSAGE_IS_QUEUED, distributionListMessageModel.isQueued())
 										.write(Tags.TAG_MESSAGE_CAPTION, distributionListMessageModel.getCaption())
 										.write(Tags.TAG_MESSAGE_CAPTION, distributionListMessageModel.getCaption())
 										.write(Tags.TAG_MESSAGE_QUOTED_MESSAGE_ID, distributionListMessageModel.getQuotedMessageId())
 										.write(Tags.TAG_MESSAGE_QUOTED_MESSAGE_ID, distributionListMessageModel.getQuotedMessageId())
+										.write(Tags.TAG_MESSAGE_DELIVERED_AT, distributionListMessageModel.getDeliveredAt())
+										.write(Tags.TAG_MESSAGE_READ_AT, distributionListMessageModel.getReadAt())
 										.write();
 										.write();
 								}
 								}
 
 

+ 1 - 1
app/src/main/java/ch/threema/app/backuprestore/csv/Helper.java

@@ -28,7 +28,7 @@ import ch.threema.app.services.GroupService;
 import ch.threema.app.services.PreferenceService;
 import ch.threema.app.services.PreferenceService;
 import ch.threema.app.services.UserService;
 import ch.threema.app.services.UserService;
 import ch.threema.app.services.ballot.BallotService;
 import ch.threema.app.services.ballot.BallotService;
-import ch.threema.client.ThreemaConnection;
+import ch.threema.domain.protocol.csp.connection.ThreemaConnection;
 import ch.threema.storage.DatabaseServiceNew;
 import ch.threema.storage.DatabaseServiceNew;
 
 
 public class Helper {
 public class Helper {

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

@@ -85,11 +85,11 @@ import ch.threema.app.utils.MimeUtil;
 import ch.threema.app.utils.StringConversionUtil;
 import ch.threema.app.utils.StringConversionUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.base.ThreemaException;
 import ch.threema.base.ThreemaException;
-import ch.threema.base.VerificationLevel;
-import ch.threema.client.GroupId;
-import ch.threema.client.ProtocolDefines;
-import ch.threema.client.ThreemaConnection;
-import ch.threema.client.Utils;
+import ch.threema.base.utils.Utils;
+import ch.threema.domain.models.GroupId;
+import ch.threema.domain.models.VerificationLevel;
+import ch.threema.domain.protocol.csp.ProtocolDefines;
+import ch.threema.domain.protocol.csp.connection.ThreemaConnection;
 import ch.threema.storage.DatabaseServiceNew;
 import ch.threema.storage.DatabaseServiceNew;
 import ch.threema.storage.factories.ContactModelFactory;
 import ch.threema.storage.factories.ContactModelFactory;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.AbstractMessageModel;
@@ -887,7 +887,7 @@ public class RestoreService extends Service {
 							if (groupService.isGroupOwner(groupModel)) {
 							if (groupService.isGroupOwner(groupModel)) {
 								groupService.sendSync(groupModel);
 								groupService.sendSync(groupModel);
 							} else {
 							} else {
-								groupService.requestSync(groupModel.getCreatorIdentity(), new GroupId(Utils.hexStringToByteArray(groupModel.getApiGroupId())));
+								groupService.requestSync(groupModel.getCreatorIdentity(), new GroupId(Utils.hexStringToByteArray(groupModel.getApiGroupId().toString())));
 							}
 							}
 						}
 						}
 					}
 					}
@@ -1020,7 +1020,7 @@ public class RestoreService extends Service {
 
 
 	private GroupModel createGroupModel(CSVRow row, RestoreSettings restoreSettings) throws ThreemaException {
 	private GroupModel createGroupModel(CSVRow row, RestoreSettings restoreSettings) throws ThreemaException {
 		GroupModel groupModel = new GroupModel();
 		GroupModel groupModel = new GroupModel();
-		groupModel.setApiGroupId(row.getString(Tags.TAG_GROUP_ID));
+		groupModel.setApiGroupId(new GroupId(row.getString(Tags.TAG_GROUP_ID)));
 		groupModel.setCreatorIdentity(row.getString(Tags.TAG_GROUP_CREATOR));
 		groupModel.setCreatorIdentity(row.getString(Tags.TAG_GROUP_CREATOR));
 		groupModel.setName(row.getString(Tags.TAG_GROUP_NAME));
 		groupModel.setName(row.getString(Tags.TAG_GROUP_NAME));
 		groupModel.setCreatedAt(row.getDate(Tags.TAG_GROUP_CREATED_AT));
 		groupModel.setCreatedAt(row.getDate(Tags.TAG_GROUP_CREATED_AT));
@@ -1501,7 +1501,10 @@ public class RestoreService extends Service {
 		else {
 		else {
 			messageModel.setIsQueued(true);
 			messageModel.setIsQueued(true);
 		}
 		}
-
+		if (restoreSettings.getVersion() >= 16) {
+			messageModel.setDeliveredAt(row.getDate(Tags.TAG_MESSAGE_DELIVERED_AT));
+			messageModel.setReadAt(row.getDate(Tags.TAG_MESSAGE_READ_AT));
+		}
 		return messageModel;
 		return messageModel;
 	}
 	}
 
 
@@ -1522,7 +1525,10 @@ public class RestoreService extends Service {
 			messageModel.setIsQueued(true);
 			messageModel.setIsQueued(true);
 		}
 		}
 		messageModel.setUid(row.getString(Tags.TAG_MESSAGE_UID));
 		messageModel.setUid(row.getString(Tags.TAG_MESSAGE_UID));
-
+		if (restoreSettings.getVersion() >= 16) {
+			messageModel.setDeliveredAt(row.getDate(Tags.TAG_MESSAGE_DELIVERED_AT));
+			messageModel.setReadAt(row.getDate(Tags.TAG_MESSAGE_READ_AT));
+		}
 		return messageModel;
 		return messageModel;
 	}
 	}
 
 
@@ -1543,6 +1549,10 @@ public class RestoreService extends Service {
 			messageModel.setIsQueued(true);
 			messageModel.setIsQueued(true);
 		}
 		}
 		messageModel.setUid(row.getString(Tags.TAG_MESSAGE_UID));
 		messageModel.setUid(row.getString(Tags.TAG_MESSAGE_UID));
+		if (restoreSettings.getVersion() >= 16) {
+			messageModel.setDeliveredAt(row.getDate(Tags.TAG_MESSAGE_DELIVERED_AT));
+			messageModel.setReadAt(row.getDate(Tags.TAG_MESSAGE_READ_AT));
+		}
 		return messageModel;
 		return messageModel;
 	}
 	}
 
 

+ 2 - 1
app/src/main/java/ch/threema/app/backuprestore/csv/RestoreSettings.java

@@ -35,8 +35,9 @@ public class RestoreSettings {
 	 * 12: voip status messages (not implemented)
 	 * 12: voip status messages (not implemented)
 	 * 13: add hidden flag to contacts
 	 * 13: add hidden flag to contacts
 	 * 15: add quoted message id to messages
 	 * 15: add quoted message id to messages
+	 * 16: added read and delivered date
 	 */
 	 */
-	public static final int CURRENT_VERSION = 15;
+	public static final int CURRENT_VERSION = 16;
 	private int version = 1;
 	private int version = 1;
 
 
 	public RestoreSettings(int version) {
 	public RestoreSettings(int version) {

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

@@ -80,6 +80,9 @@ public abstract class Tags {
 	public static final String TAG_MESSAGE_IS_SAVED = "issaved";
 	public static final String TAG_MESSAGE_IS_SAVED = "issaved";
 	public static final String TAG_MESSAGE_CREATED_AT = "created_at";
 	public static final String TAG_MESSAGE_CREATED_AT = "created_at";
 	public static final String TAG_MESSAGE_MODIFIED_AT = "modified_at";
 	public static final String TAG_MESSAGE_MODIFIED_AT = "modified_at";
+	public static final String TAG_MESSAGE_DELIVERED_AT = "delivered_at";
+	public static final String TAG_MESSAGE_READ_AT = "read_at";
+
 	public static final String TAG_MESSAGE_MESSAGE_STATE = "messagestae";
 	public static final String TAG_MESSAGE_MESSAGE_STATE = "messagestae";
 	public static final String TAG_MESSAGE_IS_STATUS_MESSAGE = "isstatusmessage";
 	public static final String TAG_MESSAGE_IS_STATUS_MESSAGE = "isstatusmessage";
 	public static final String TAG_MESSAGE_IS_QUEUED = "isqueued";
 	public static final String TAG_MESSAGE_IS_QUEUED = "isqueued";

+ 8 - 15
app/src/main/java/ch/threema/app/camera/CameraActivity.java

@@ -44,7 +44,6 @@ import java.io.IOException;
 
 
 import androidx.annotation.NonNull;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
 import androidx.camera.lifecycle.ProcessCameraProvider;
 import androidx.camera.lifecycle.ProcessCameraProvider;
 import androidx.core.content.ContextCompat;
 import androidx.core.content.ContextCompat;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.Fragment;
@@ -58,8 +57,6 @@ import ch.threema.app.utils.ConfigUtils;
 import static android.view.KeyEvent.KEYCODE_VOLUME_DOWN;
 import static android.view.KeyEvent.KEYCODE_VOLUME_DOWN;
 import static android.view.KeyEvent.KEYCODE_VOLUME_UP;
 import static android.view.KeyEvent.KEYCODE_VOLUME_UP;
 
 
-
-@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
 public class CameraActivity extends ThreemaAppCompatActivity implements CameraFragment.CameraCallback, CameraFragment.CameraConfiguration {
 public class CameraActivity extends ThreemaAppCompatActivity implements CameraFragment.CameraCallback, CameraFragment.CameraConfiguration {
 	private static final Logger logger = LoggerFactory.getLogger(CameraActivity.class);
 	private static final Logger logger = LoggerFactory.getLogger(CameraActivity.class);
 
 
@@ -80,18 +77,14 @@ public class CameraActivity extends ThreemaAppCompatActivity implements CameraFr
 
 
 		setContentView(R.layout.camerax_activity_camera);
 		setContentView(R.layout.camerax_activity_camera);
 
 
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-			getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
-			getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
-			getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
-			getWindow().setStatusBarColor(Color.TRANSPARENT);
-			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-				// we want dark icons, i.e. a light status bar
-				getWindow().getDecorView().setSystemUiVisibility(
-						getWindow().getDecorView().getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
-			}
-		} else {
-			getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+		getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+		getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+		getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
+		getWindow().setStatusBarColor(Color.TRANSPARENT);
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+			// we want dark icons, i.e. a light status bar
+			getWindow().getDecorView().setSystemUiVisibility(
+					getWindow().getDecorView().getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
 		}
 		}
 
 
 		if (getIntent() != null) {
 		if (getIntent() != null) {

+ 0 - 3
app/src/main/java/ch/threema/app/camera/CameraFragment.java

@@ -31,7 +31,6 @@ import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManager;
 import android.os.AsyncTask;
 import android.os.AsyncTask;
-import android.os.Build;
 import android.os.Bundle;
 import android.os.Bundle;
 import android.text.format.DateUtils;
 import android.text.format.DateUtils;
 import android.view.KeyEvent;
 import android.view.KeyEvent;
@@ -56,7 +55,6 @@ import java.util.concurrent.Executors;
 
 
 import androidx.annotation.NonNull;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
 import androidx.camera.core.Camera;
 import androidx.camera.core.Camera;
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.ImageCapture;
@@ -86,7 +84,6 @@ import static ch.threema.app.camera.CameraActivity.KEY_EVENT_ACTION;
 import static ch.threema.app.camera.CameraActivity.KEY_EVENT_EXTRA;
 import static ch.threema.app.camera.CameraActivity.KEY_EVENT_EXTRA;
 
 
 @SuppressWarnings("deprecation")
 @SuppressWarnings("deprecation")
-@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
 public class CameraFragment extends Fragment {
 public class CameraFragment extends Fragment {
 	private static final Logger logger = LoggerFactory.getLogger(CameraFragment.class);
 	private static final Logger logger = LoggerFactory.getLogger(CameraFragment.class);
 	private static final int PERMISSION_REQUEST_CODE_AUDIO = 869;
 	private static final int PERMISSION_REQUEST_CODE_AUDIO = 869;

+ 1 - 5
app/src/main/java/ch/threema/app/camera/CameraUtil.java

@@ -35,7 +35,6 @@ import java.nio.ByteBuffer;
 import java.util.HashSet;
 import java.util.HashSet;
 
 
 import androidx.annotation.NonNull;
 import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.ImageProxy;
 import androidx.camera.core.ImageProxy;
 
 
@@ -54,7 +53,6 @@ public class CameraUtil {
 	private static final HashSet<String> BLACKLISTED_CAMERAS = new HashSet<String>() {{
 	private static final HashSet<String> BLACKLISTED_CAMERAS = new HashSet<String>() {{
 	}};
 	}};
 
 
-	@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
 	private static byte[] transformByteArray(@NonNull byte[] data, Rect cropRect, int rotation, boolean flip) throws IOException {
 	private static byte[] transformByteArray(@NonNull byte[] data, Rect cropRect, int rotation, boolean flip) throws IOException {
 		Bitmap in = null;
 		Bitmap in = null;
 
 
@@ -95,7 +93,6 @@ public class CameraUtil {
 		return null;
 		return null;
 	}
 	}
 
 
-	@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
 	static byte[] getJpegBytes(@NonNull ImageProxy image, int rotation, boolean flip) throws IOException {
 	static byte[] getJpegBytes(@NonNull ImageProxy image, int rotation, boolean flip) throws IOException {
 		ImageProxy.PlaneProxy[] planes = image.getPlanes();
 		ImageProxy.PlaneProxy[] planes = image.getPlanes();
 		ByteBuffer buffer = planes[0].getBuffer();
 		ByteBuffer buffer = planes[0].getBuffer();
@@ -121,7 +118,6 @@ public class CameraUtil {
 		return out.toByteArray();
 		return out.toByteArray();
 	}
 	}
 
 
-	@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
 	private static boolean shouldCropImage(@NonNull ImageProxy image) {
 	private static boolean shouldCropImage(@NonNull ImageProxy image) {
 		Size sourceSize = new Size(image.getWidth(), image.getHeight());
 		Size sourceSize = new Size(image.getWidth(), image.getHeight());
 		Size targetSize = new Size(image.getCropRect().width(), image.getCropRect().height());
 		Size targetSize = new Size(image.getCropRect().width(), image.getCropRect().height());
@@ -138,6 +134,6 @@ public class CameraUtil {
 	 * @return
 	 * @return
 	 */
 	 */
 	public static boolean isInternalCameraSupported() {
 	public static boolean isInternalCameraSupported() {
-		return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !BLACKLISTED_CAMERAS.contains(Build.MODEL);
+		return !BLACKLISTED_CAMERAS.contains(Build.MODEL);
 	}
 	}
 }
 }

+ 1 - 3
app/src/main/java/ch/threema/app/camera/VideoEditView.java

@@ -61,7 +61,6 @@ import java.io.File;
 
 
 import androidx.annotation.MainThread;
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
 import androidx.annotation.UiThread;
 import androidx.annotation.UiThread;
 import androidx.lifecycle.DefaultLifecycleObserver;
 import androidx.lifecycle.DefaultLifecycleObserver;
 import androidx.lifecycle.LifecycleOwner;
 import androidx.lifecycle.LifecycleOwner;
@@ -143,7 +142,7 @@ public class VideoEditView extends FrameLayout implements DefaultLifecycleObserv
 		this.dimPaint = new Paint();
 		this.dimPaint = new Paint();
 
 
 		this.dimPaint.setStyle(Paint.Style.FILL);
 		this.dimPaint.setStyle(Paint.Style.FILL);
-		this.dimPaint.setColor(context.getResources().getColor(R.color.background_dim_dark));
+		this.dimPaint.setColor(context.getResources().getColor(R.color.dark_background_dim));
 		this.dimPaint.setAntiAlias(false);
 		this.dimPaint.setAntiAlias(false);
 		this.dimPaint.setStrokeWidth(0);
 		this.dimPaint.setStrokeWidth(0);
 
 
@@ -358,7 +357,6 @@ public class VideoEditView extends FrameLayout implements DefaultLifecycleObserv
 	}
 	}
 
 
 	@SuppressLint("StaticFieldLeak")
 	@SuppressLint("StaticFieldLeak")
-	@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
 	@UiThread
 	@UiThread
 	public void setVideo(MediaItem mediaItem) {
 	public void setVideo(MediaItem mediaItem) {
 		int numColumns = calculateNumColumns();
 		int numColumns = calculateNumColumns();

+ 1 - 4
app/src/main/java/ch/threema/app/camera/ZoomView.java

@@ -25,16 +25,13 @@ import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Color;
 import android.graphics.Paint;
 import android.graphics.Paint;
-import android.os.Build;
 import android.util.AttributeSet;
 import android.util.AttributeSet;
 import android.widget.FrameLayout;
 import android.widget.FrameLayout;
 
 
 import androidx.annotation.NonNull;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
 import ch.threema.app.R;
 import ch.threema.app.R;
 
 
-@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
 public class ZoomView extends FrameLayout {
 public class ZoomView extends FrameLayout {
 
 
 	private Paint linePaint, circlePaint, semiPaint, labelPaint;
 	private Paint linePaint, circlePaint, semiPaint, labelPaint;
@@ -71,7 +68,7 @@ public class ZoomView extends FrameLayout {
 
 
 		this.semiPaint = new Paint();
 		this.semiPaint = new Paint();
 		this.semiPaint.setStyle(Paint.Style.STROKE);
 		this.semiPaint.setStyle(Paint.Style.STROKE);
-		this.semiPaint.setColor(getResources().getColor(R.color.background_dim_light));
+		this.semiPaint.setColor(getResources().getColor(R.color.background_dim));
 		this.semiPaint.setAntiAlias(true);
 		this.semiPaint.setAntiAlias(true);
 		this.semiPaint.setStrokeWidth(this.strokeWidth);
 		this.semiPaint.setStrokeWidth(this.strokeWidth);
 
 

+ 2 - 2
app/src/main/java/ch/threema/app/dialogs/ContactEditDialog.java

@@ -52,7 +52,7 @@ import ch.threema.app.ui.AvatarEditView;
 import ch.threema.app.utils.ContactUtil;
 import ch.threema.app.utils.ContactUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.ViewUtil;
 import ch.threema.app.utils.ViewUtil;
-import ch.threema.base.Contact;
+import ch.threema.domain.models.Contact;
 import ch.threema.localcrypto.MasterKeyLockedException;
 import ch.threema.localcrypto.MasterKeyLockedException;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.ContactModel;
 
 
@@ -239,7 +239,7 @@ public class ContactEditDialog extends ThreemaDialogFragment implements AvatarEd
 		try {
 		try {
 			contactService = ThreemaApplication.getServiceManager().getContactService();
 			contactService = ThreemaApplication.getServiceManager().getContactService();
 			groupService = ThreemaApplication.getServiceManager().getGroupService();
 			groupService = ThreemaApplication.getServiceManager().getGroupService();
-		} catch (MasterKeyLockedException | FileSystemNotPresentException | NoIdentityException e) {
+		} catch (MasterKeyLockedException | FileSystemNotPresentException e) {
 			logger.error("Exception", e);
 			logger.error("Exception", e);
 		}
 		}
 
 

+ 29 - 12
app/src/main/java/ch/threema/app/dialogs/MessageDetailDialog.java

@@ -90,8 +90,10 @@ public class MessageDetailDialog extends ThreemaDialogFragment {
 			final TextView createdDate = dialogView.findViewById(R.id.created_date);
 			final TextView createdDate = dialogView.findViewById(R.id.created_date);
 			final TextView postedText = dialogView.findViewById(R.id.posted_text);
 			final TextView postedText = dialogView.findViewById(R.id.posted_text);
 			final TextView postedDate = dialogView.findViewById(R.id.posted_date);
 			final TextView postedDate = dialogView.findViewById(R.id.posted_date);
-			final TextView modifiedText = dialogView.findViewById(R.id.modified_text);
-			final TextView modifiedDate = dialogView.findViewById(R.id.modified_date);
+			final TextView deliveredText = dialogView.findViewById(R.id.delivered_text);
+			final TextView deliveredDate = dialogView.findViewById(R.id.delivered_date);
+			final TextView readText = dialogView.findViewById(R.id.read_text);
+			final TextView readDate = dialogView.findViewById(R.id.read_date);
 			final TextView messageIdText = dialogView.findViewById(R.id.messageid_text);
 			final TextView messageIdText = dialogView.findViewById(R.id.messageid_text);
 			final TextView messageIdDate = dialogView.findViewById(R.id.messageid_date);
 			final TextView messageIdDate = dialogView.findViewById(R.id.messageid_date);
 			final TextView mimeTypeText = dialogView.findViewById(R.id.filetype_text);
 			final TextView mimeTypeText = dialogView.findViewById(R.id.filetype_text);
@@ -136,13 +138,28 @@ public class MessageDetailDialog extends ThreemaDialogFragment {
 					}
 					}
 
 
 					if (messageState != MessageState.SENT && !(messageModel.getType() == MessageType.BALLOT && messageModel instanceof GroupMessageModel)) {
 					if (messageState != MessageState.SENT && !(messageModel.getType() == MessageType.BALLOT && messageModel instanceof GroupMessageModel)) {
+						Date deliveredAt = messageModel.getDeliveredAt();
+						Date readAt = messageModel.getReadAt();
 						Date modifiedAt = messageModel.getModifiedAt();
 						Date modifiedAt = messageModel.getModifiedAt();
-						modifiedText.setText(TextUtil.capitalize(getString(stateResource)));
-						modifiedDate.setText(modifiedAt != null ?
-							LocaleUtil.formatTimeStampStringAbsolute(getContext(), messageModel.getModifiedAt().getTime()) :
-							(messageModel.getPostedAt(true) != null ? LocaleUtil.formatTimeStampStringAbsolute(getContext(), messageModel.getPostedAt(true).getTime()) : ""));
-						modifiedText.setVisibility(View.VISIBLE);
-						modifiedDate.setVisibility(View.VISIBLE);
+
+						if (readAt != null && deliveredAt != null) {
+							deliveredText.setText(TextUtil.capitalize(getString(R.string.state_delivered)));
+							deliveredDate.setText(LocaleUtil.formatTimeStampStringAbsolute(getContext(), deliveredAt.getTime()));
+							deliveredText.setVisibility(View.VISIBLE);
+							deliveredDate.setVisibility(View.VISIBLE);
+
+							readText.setText(TextUtil.capitalize(getString(R.string.state_read)));
+							readDate.setText(LocaleUtil.formatTimeStampStringAbsolute(getContext(), readAt.getTime()));
+							readText.setVisibility(View.VISIBLE);
+							readDate.setVisibility(View.VISIBLE);
+						} else {
+							deliveredText.setText(TextUtil.capitalize(getString(stateResource)));
+							deliveredDate.setText(modifiedAt != null ?
+								LocaleUtil.formatTimeStampStringAbsolute(getContext(), messageModel.getModifiedAt().getTime()) :
+								(messageModel.getPostedAt(true) != null ? LocaleUtil.formatTimeStampStringAbsolute(getContext(), messageModel.getPostedAt(true).getTime()) : ""));
+							deliveredText.setVisibility(View.VISIBLE);
+							deliveredDate.setVisibility(View.VISIBLE);
+						}
 					}
 					}
 				} else {
 				} else {
 					// incoming msgs
 					// incoming msgs
@@ -160,10 +177,10 @@ public class MessageDetailDialog extends ThreemaDialogFragment {
 						postedDate.setVisibility(View.VISIBLE);
 						postedDate.setVisibility(View.VISIBLE);
 					}
 					}
 					if (messageModel.getModifiedAt() != null && messageState != MessageState.READ) {
 					if (messageModel.getModifiedAt() != null && messageState != MessageState.READ) {
-						modifiedText.setText(TextUtil.capitalize(getString(R.string.state_read)));
-						modifiedDate.setText(LocaleUtil.formatTimeStampStringAbsolute(getContext(), messageModel.getModifiedAt().getTime()));
-						modifiedText.setVisibility(View.VISIBLE);
-						modifiedDate.setVisibility(View.VISIBLE);
+						deliveredText.setText(TextUtil.capitalize(getString(R.string.state_read)));
+						deliveredDate.setText(LocaleUtil.formatTimeStampStringAbsolute(getContext(), messageModel.getModifiedAt().getTime()));
+						deliveredText.setVisibility(View.VISIBLE);
+						deliveredDate.setVisibility(View.VISIBLE);
 					}
 					}
 				}
 				}
 
 

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

@@ -38,7 +38,7 @@ import androidx.appcompat.app.AlertDialog;
 import androidx.appcompat.app.AppCompatDialog;
 import androidx.appcompat.app.AppCompatDialog;
 import ch.threema.app.R;
 import ch.threema.app.R;
 import ch.threema.app.emojis.EmojiEditText;
 import ch.threema.app.emojis.EmojiEditText;
-import ch.threema.client.ProtocolDefines;
+import ch.threema.domain.protocol.csp.ProtocolDefines;
 
 
 public class NewContactDialog extends ThreemaDialogFragment {
 public class NewContactDialog extends ThreemaDialogFragment {
 	private NewContactDialogClickListener callback;
 	private NewContactDialogClickListener callback;

+ 32 - 4
app/src/main/java/ch/threema/app/dialogs/PasswordEntryDialog.java

@@ -26,6 +26,7 @@ import android.content.DialogInterface;
 import android.content.res.ColorStateList;
 import android.content.res.ColorStateList;
 import android.os.Bundle;
 import android.os.Bundle;
 import android.text.Editable;
 import android.text.Editable;
+import android.text.Html;
 import android.text.InputFilter;
 import android.text.InputFilter;
 import android.text.InputType;
 import android.text.InputType;
 import android.text.SpannableString;
 import android.text.SpannableString;
@@ -48,6 +49,7 @@ import androidx.appcompat.app.AppCompatDialog;
 import androidx.core.text.util.LinkifyCompat;
 import androidx.core.text.util.LinkifyCompat;
 import ch.threema.app.R;
 import ch.threema.app.R;
 import ch.threema.app.utils.DialogUtil;
 import ch.threema.app.utils.DialogUtil;
+import ch.threema.app.utils.LocaleUtil;
 
 
 public class PasswordEntryDialog extends ThreemaDialogFragment implements GenericAlertDialog.DialogClickListener {
 public class PasswordEntryDialog extends ThreemaDialogFragment implements GenericAlertDialog.DialogClickListener {
 	private static final String DIALOG_TAG_CONFIRM_CHECKBOX = "dtcc";
 	private static final String DIALOG_TAG_CONFIRM_CHECKBOX = "dtcc";
@@ -59,12 +61,18 @@ public class PasswordEntryDialog extends ThreemaDialogFragment implements Generi
 	protected boolean isLengthCheck = true;
 	protected boolean isLengthCheck = true;
 	protected int minLength, maxLength;
 	protected int minLength, maxLength;
 	protected MaterialCheckBox checkBox;
 	protected MaterialCheckBox checkBox;
+	public enum ForgotHintType {
+		NONE,
+		SAFE,
+		PIN_PASSPHRASE
+	}
 
 
 	public static PasswordEntryDialog newInstance(@StringRes int title, @StringRes int message,
 	public static PasswordEntryDialog newInstance(@StringRes int title, @StringRes int message,
 	                                              @StringRes int hint,
 	                                              @StringRes int hint,
 	                                              @StringRes int positive, @StringRes int negative,
 	                                              @StringRes int positive, @StringRes int negative,
 	                                              int minLength, int maxLength,
 	                                              int minLength, int maxLength,
-	                                              int confirmHint, int inputType, int checkboxText) {
+	                                              int confirmHint, int inputType, int checkboxText,
+	                                              ForgotHintType showForgotPwHint ) {
 		PasswordEntryDialog dialog = new PasswordEntryDialog();
 		PasswordEntryDialog dialog = new PasswordEntryDialog();
 		Bundle args = new Bundle();
 		Bundle args = new Bundle();
 		args.putInt("title", title);
 		args.putInt("title", title);
@@ -77,6 +85,7 @@ public class PasswordEntryDialog extends ThreemaDialogFragment implements Generi
 		args.putInt("confirmHint", confirmHint);
 		args.putInt("confirmHint", confirmHint);
 		args.putInt("inputType", inputType);
 		args.putInt("inputType", inputType);
 		args.putInt("checkboxText", checkboxText);
 		args.putInt("checkboxText", checkboxText);
+		args.putSerializable("showForgotPwHint", showForgotPwHint);
 
 
 		dialog.setArguments(args);
 		dialog.setArguments(args);
 		return dialog;
 		return dialog;
@@ -106,8 +115,7 @@ public class PasswordEntryDialog extends ThreemaDialogFragment implements Generi
 	}
 	}
 
 
 	@Override
 	@Override
-	public void onYes(String tag, Object data) {
-	}
+	public void onYes(String tag, Object data) { }
 
 
 	@Override
 	@Override
 	public void onNo(String tag, Object data) {
 	public void onNo(String tag, Object data) {
@@ -152,7 +160,6 @@ public class PasswordEntryDialog extends ThreemaDialogFragment implements Generi
 			return alertDialog;
 			return alertDialog;
 		}
 		}
 
 
-
 		final int title = getArguments().getInt("title");
 		final int title = getArguments().getInt("title");
 		int message = getArguments().getInt("message");
 		int message = getArguments().getInt("message");
 		int hint = getArguments().getInt("hint");
 		int hint = getArguments().getInt("hint");
@@ -164,6 +171,7 @@ public class PasswordEntryDialog extends ThreemaDialogFragment implements Generi
 		final int confirmHint = getArguments().getInt("confirmHint", 0);
 		final int confirmHint = getArguments().getInt("confirmHint", 0);
 		final int checkboxText = getArguments().getInt("checkboxText", 0);
 		final int checkboxText = getArguments().getInt("checkboxText", 0);
 		final int checkboxConfirmText = getArguments().getInt("checkboxConfirmText", 0);
 		final int checkboxConfirmText = getArguments().getInt("checkboxConfirmText", 0);
+		final ForgotHintType showForgotPwHint = (ForgotHintType) getArguments().getSerializable("showForgotPwHint");
 
 
 		final String tag = this.getTag();
 		final String tag = this.getTag();
 
 
@@ -172,6 +180,7 @@ public class PasswordEntryDialog extends ThreemaDialogFragment implements Generi
 
 
 		final View dialogView = activity.getLayoutInflater().inflate(R.layout.dialog_password_entry, null);
 		final View dialogView = activity.getLayoutInflater().inflate(R.layout.dialog_password_entry, null);
 		final TextView messageTextView = dialogView.findViewById(R.id.message_text);
 		final TextView messageTextView = dialogView.findViewById(R.id.message_text);
+		final TextView forgotPwTextView = dialogView.findViewById(R.id.forgot_password);
 		final TextInputEditText editText1 = dialogView.findViewById(R.id.password1);
 		final TextInputEditText editText1 = dialogView.findViewById(R.id.password1);
 		final TextInputEditText editText2 = dialogView.findViewById(R.id.password2);
 		final TextInputEditText editText2 = dialogView.findViewById(R.id.password2);
 		final TextInputLayout editText1Layout = dialogView.findViewById(R.id.password1layout);
 		final TextInputLayout editText1Layout = dialogView.findViewById(R.id.password1layout);
@@ -243,6 +252,25 @@ public class PasswordEntryDialog extends ThreemaDialogFragment implements Generi
 			editText1Layout.setHelperText(String.format(activity.getString(R.string.password_too_short), minLength));
 			editText1Layout.setHelperText(String.format(activity.getString(R.string.password_too_short), minLength));
 		}
 		}
 
 
+		if (showForgotPwHint != null) {
+			switch (showForgotPwHint) {
+				case SAFE:
+					String safeFaqUrl = String.format(getString(R.string.threema_safe_password_faq), LocaleUtil.getAppLanguage());
+					forgotPwTextView.setText(Html.fromHtml(String.format(getString(R.string.forgot_your_password), safeFaqUrl)));
+					forgotPwTextView.setMovementMethod(LinkMovementMethod.getInstance());
+					forgotPwTextView.setVisibility(View.VISIBLE);
+					break;
+				case PIN_PASSPHRASE:
+					String pinFaqUrl = String.format(getString(R.string.threema_passwords_faq), LocaleUtil.getAppLanguage());
+					forgotPwTextView.setText(Html.fromHtml(String.format(getString(R.string.forgot_your_password), pinFaqUrl)));
+					forgotPwTextView.setMovementMethod(LinkMovementMethod.getInstance());
+					forgotPwTextView.setVisibility(View.VISIBLE);
+					break;
+				case NONE:
+					break;
+			}
+		}
+
 		MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity(), getTheme());
 		MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity(), getTheme());
 
 
 		if (title != 0) {
 		if (title != 0) {

+ 109 - 0
app/src/main/java/ch/threema/app/dialogs/PublicKeyDialog.java

@@ -0,0 +1,109 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2014-2021 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.app.dialogs;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatDialog;
+import ch.threema.app.R;
+import ch.threema.app.ThreemaApplication;
+import ch.threema.app.managers.ServiceManager;
+import ch.threema.base.utils.Utils;
+
+import static android.content.Context.CLIPBOARD_SERVICE;
+
+public class PublicKeyDialog extends SimpleStringAlertDialog {
+	private String publicKeyString;
+	private CharSequence title;
+
+	public static PublicKeyDialog newInstance(CharSequence title, byte[] publicKey) {
+		PublicKeyDialog dialog = new PublicKeyDialog();
+		Bundle args = new Bundle();
+		args.putCharSequence("title", title);
+		args.putByteArray("publicKey", publicKey);
+
+		dialog.setArguments(args);
+		return dialog;
+	}
+
+	@Override
+	public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v, @Nullable ContextMenu.ContextMenuInfo menuInfo) {
+		MenuItem menuItem = menu.add(0, v.getId(), 0, getContext().getString(R.string.copy));
+		menuItem.setOnMenuItemClickListener(item -> {
+			ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(CLIPBOARD_SERVICE);
+
+			ServiceManager serviceManager = ThreemaApplication.getServiceManager();
+			if (serviceManager != null) {
+				ClipData clip = ClipData.newPlainText(title, publicKeyString);
+				clipboard.setPrimaryClip(clip);
+			}
+
+			Toast.makeText(getContext(), R.string.copied, Toast.LENGTH_SHORT).show();
+
+			return true;
+		});
+	}
+
+	@NonNull
+	@Override
+	public AppCompatDialog onCreateDialog(Bundle savedInstanceState) {
+		title = getArguments().getCharSequence("title");
+		byte[] publicKey = getArguments().getByteArray("publicKey");
+		publicKeyString = Utils.byteArrayToHexString(publicKey);
+
+		final View dialogView = activity.getLayoutInflater().inflate(R.layout.dialog_public_key, null);
+		final TextView messageView = dialogView.findViewById(R.id.message);
+
+		registerForContextMenu(messageView);
+
+		StringBuilder message = new StringBuilder();
+
+		for(int i = 0; i < publicKeyString.length(); i++) {
+			if (i != 0 && i % 8 == 0) {
+				message.append("\n");
+			}
+			message.append(publicKeyString.charAt(i));
+		}
+
+		MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity(), getTheme())
+			.setCancelable(false)
+			.setView(dialogView)
+			.setTitle(title)
+			.setPositiveButton(getString(R.string.ok), null)
+			.setCancelable(false);
+
+		messageView.setText(message);
+
+		return builder.create();
+	}
+}

+ 4 - 2
app/src/main/java/ch/threema/app/dialogs/SMSVerificationDialog.java

@@ -32,6 +32,7 @@ import android.widget.EditText;
 
 
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 
 
+import androidx.annotation.NonNull;
 import androidx.appcompat.app.AlertDialog;
 import androidx.appcompat.app.AlertDialog;
 import androidx.appcompat.app.AppCompatDialog;
 import androidx.appcompat.app.AppCompatDialog;
 import ch.threema.app.R;
 import ch.threema.app.R;
@@ -80,12 +81,13 @@ public class SMSVerificationDialog extends ThreemaDialogFragment {
 	}
 	}
 
 
 	@Override
 	@Override
-	public void onAttach(Activity activity) {
+	public void onAttach(@NonNull Activity activity) {
 		super.onAttach(activity);
 		super.onAttach(activity);
 
 
 		this.activity = activity;
 		this.activity = activity;
 	}
 	}
 
 
+	@NonNull
 	@Override
 	@Override
 	public AppCompatDialog onCreateDialog(Bundle savedInstanceState) {
 	public AppCompatDialog onCreateDialog(Bundle savedInstanceState) {
 		String phone = getArguments().getString(ARG_PHONE_NUMBER);
 		String phone = getArguments().getString(ARG_PHONE_NUMBER);
@@ -126,7 +128,7 @@ public class SMSVerificationDialog extends ThreemaDialogFragment {
 	}
 	}
 
 
 	@Override
 	@Override
-	public void onCancel(DialogInterface dialogInterface) {
+	public void onCancel(@NonNull DialogInterface dialogInterface) {
 		callback.onNo(tag);
 		callback.onNo(tag);
 	}
 	}
 
 

+ 73 - 56
app/src/main/java/ch/threema/app/dialogs/SelectorDialog.java

@@ -25,6 +25,11 @@ import android.app.Activity;
 import android.content.DialogInterface;
 import android.content.DialogInterface;
 import android.os.Bundle;
 import android.os.Bundle;
 import android.os.Parcelable;
 import android.os.Parcelable;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ListAdapter;
+import android.widget.TextView;
 
 
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 
 
@@ -33,6 +38,8 @@ import java.util.ArrayList;
 import androidx.annotation.NonNull;
 import androidx.annotation.NonNull;
 import androidx.appcompat.app.AlertDialog;
 import androidx.appcompat.app.AlertDialog;
 import androidx.appcompat.app.AppCompatDialog;
 import androidx.appcompat.app.AppCompatDialog;
+import ch.threema.app.R;
+import ch.threema.app.ui.SelectorDialogItem;
 
 
 public class SelectorDialog extends ThreemaDialogFragment {
 public class SelectorDialog extends ThreemaDialogFragment {
 	private SelectorDialogClickListener callback;
 	private SelectorDialogClickListener callback;
@@ -40,51 +47,42 @@ public class SelectorDialog extends ThreemaDialogFragment {
 	private Activity activity;
 	private Activity activity;
 	private AlertDialog alertDialog;
 	private AlertDialog alertDialog;
 
 
-	public static SelectorDialog newInstance(String title, ArrayList<String> items, String negative, SelectorDialogInlineClickListener listener) {
-		// do not use inline callbacks in activities that don't have android:configChanges="orientation|screenSize|keyboardHidden" set
-		// or fragments without setRetainInstance(true)
-		SelectorDialog dialog = new SelectorDialog();
-		Bundle args = new Bundle();
-		args.putString("title", title);
-		args.putStringArrayList("items", items);
-		args.putString("negative", negative);
-		args.putParcelable("listener", listener);
-
-		dialog.setArguments(args);
-		return dialog;
-	}
+	private static final String BUNDLE_TITLE_EXTRA = "title";
+	private static final String BUNDLE_ITEMS_EXTRA = "items";
+	private static final String BUNDLE_VALUES_EXTRA = "values";
+	private static final String BUNDLE_NEGATIVE_EXTRA = "negative";
+	private static final String BUNDLE_LISTENER_EXTRA = "listener";
 
 
-	public static SelectorDialog newInstance(String title, ArrayList<String> items, String negative) {
+	public static SelectorDialog newInstance(String title, ArrayList<SelectorDialogItem> items, String negative) {
 		SelectorDialog dialog = new SelectorDialog();
 		SelectorDialog dialog = new SelectorDialog();
 		Bundle args = new Bundle();
 		Bundle args = new Bundle();
-		args.putString("title", title);
-		args.putStringArrayList("items", items);
-		args.putString("negative", negative);
+		args.putString(BUNDLE_TITLE_EXTRA, title);
+		args.putSerializable(BUNDLE_ITEMS_EXTRA, items);
+		args.putString(BUNDLE_NEGATIVE_EXTRA, negative);
 
 
 		dialog.setArguments(args);
 		dialog.setArguments(args);
 		return dialog;
 		return dialog;
 	}
 	}
 
 
-	public static SelectorDialog newInstance(String title, ArrayList<String> items, ArrayList<Integer> values, String negative) {
+	public static SelectorDialog newInstance(String title, ArrayList<SelectorDialogItem> items, ArrayList<Integer> values, String negative) {
 		SelectorDialog dialog = new SelectorDialog();
 		SelectorDialog dialog = new SelectorDialog();
 		Bundle args = new Bundle();
 		Bundle args = new Bundle();
-		args.putString("title", title);
-		args.putIntegerArrayList("values", values);
-		args.putStringArrayList("items", items);
-		args.putString("negative", negative);
+		args.putString(BUNDLE_TITLE_EXTRA, title);
+		args.putIntegerArrayList(BUNDLE_VALUES_EXTRA, values);
+		args.putSerializable(BUNDLE_ITEMS_EXTRA, items);
+		args.putString(BUNDLE_NEGATIVE_EXTRA, negative);
 
 
 		dialog.setArguments(args);
 		dialog.setArguments(args);
 		return dialog;
 		return dialog;
 	}
 	}
 
 
-	public static SelectorDialog newInstance(String title, ArrayList<String> items, ArrayList<Integer> values, String negative, SelectorDialogInlineClickListener listener) {
+	public static SelectorDialog newInstance(String title, ArrayList<SelectorDialogItem> items, String negative, SelectorDialogInlineClickListener listener) {
 		SelectorDialog dialog = new SelectorDialog();
 		SelectorDialog dialog = new SelectorDialog();
 		Bundle args = new Bundle();
 		Bundle args = new Bundle();
-		args.putString("title", title);
-		args.putIntegerArrayList("values", values);
-		args.putStringArrayList("items", items);
-		args.putString("negative", negative);
-		args.putParcelable("listener", listener);
+		args.putString(BUNDLE_TITLE_EXTRA, title);
+		args.putSerializable(BUNDLE_ITEMS_EXTRA, items);
+		args.putString(BUNDLE_NEGATIVE_EXTRA, negative);
+		args.putParcelable(BUNDLE_LISTENER_EXTRA, listener);
 
 
 		dialog.setArguments(args);
 		dialog.setArguments(args);
 		return dialog;
 		return dialog;
@@ -123,11 +121,12 @@ public class SelectorDialog extends ThreemaDialogFragment {
 	@NonNull
 	@NonNull
 	@Override
 	@Override
 	public AppCompatDialog onCreateDialog(Bundle savedInstanceState) {
 	public AppCompatDialog onCreateDialog(Bundle savedInstanceState) {
-		String title = getArguments().getString("title");
-		final ArrayList<String> items = getArguments().getStringArrayList("items");
-		final ArrayList<Integer> values = getArguments().getIntegerArrayList("values");
-		String negative = getArguments().getString("negative");
-		SelectorDialogInlineClickListener listener = getArguments().getParcelable("listener");
+		Bundle arguments = getArguments();
+		String title = arguments.getString("title");
+		final ArrayList<SelectorDialogItem> items = (ArrayList<SelectorDialogItem>) arguments.getSerializable("items");
+		final ArrayList<Integer> values = arguments.getIntegerArrayList("values");
+		String negative = arguments.getString("negative");
+		SelectorDialogInlineClickListener listener = arguments.getParcelable("listener");
 
 
 		if (listener != null) {
 		if (listener != null) {
 			inlineCallback = listener;
 			inlineCallback = listener;
@@ -139,36 +138,54 @@ public class SelectorDialog extends ThreemaDialogFragment {
 		if (title != null) {
 		if (title != null) {
 			builder.setTitle(title);
 			builder.setTitle(title);
 		}
 		}
-		builder.setItems(items.toArray(new String[0]), new DialogInterface.OnClickListener() {
+
+
+		ListAdapter adapter = new ArrayAdapter<SelectorDialogItem> (
+			activity,
+			R.layout.item_selector_dialog,
+			R.id.text1,
+			items){
 			@Override
 			@Override
-			public void onClick(DialogInterface dialog, int which) {
-				dialog.dismiss();
+			public View getView(int position, View convertView, ViewGroup parent) {
+				//Use super class to create the View
+				View v = super.getView(position, convertView, parent);
+				TextView selectorOptionDesc = v.findViewById(R.id.text1);
+
+				//Put the image on the TextView
+				selectorOptionDesc.setCompoundDrawablesWithIntrinsicBounds(items.get(position).getIcon(), 0, 0, 0);
 
 
-				if (values != null && values.size() > 0) {
-					if (inlineCallback != null) {
-						inlineCallback.onClick(tag, values.get(which), object);
-					} else {
-						callback.onClick(tag, values.get(which), object);
-					}
+				//Add margin between image and text (support various screen densities)
+				selectorOptionDesc.setCompoundDrawablePadding(getResources().getDimensionPixelSize(R.dimen.listitem_standard_margin_left_right));
+
+				return v;
+			}
+		};
+
+		builder.setAdapter(adapter, (dialog, which) -> {
+			dialog.dismiss();
+
+			if (values != null && values.size() > 0) {
+				if (inlineCallback != null) {
+					inlineCallback.onClick(tag, values.get(which), object);
 				} else {
 				} else {
-					if (inlineCallback != null) {
-						inlineCallback.onClick(tag, which, object);
-					} else {
-						callback.onClick(tag, which, object);
-					}
+					callback.onClick(tag, values.get(which), object);
+				}
+			} else {
+				if (inlineCallback != null) {
+					inlineCallback.onClick(tag, which, object);
+				} else {
+					callback.onClick(tag, which, object);
 				}
 				}
 			}
 			}
 		});
 		});
+
 		if (negative != null) {
 		if (negative != null) {
-			builder.setNegativeButton(negative, new DialogInterface.OnClickListener() {
-				@Override
-				public void onClick(DialogInterface dialog, int which) {
-					dialog.dismiss();
-					if (inlineCallback != null) {
-						inlineCallback.onNo(tag);
-					} else {
-						callback.onNo(tag);
-					}
+			builder.setNegativeButton(negative, (dialog, which) -> {
+				dialog.dismiss();
+				if (inlineCallback != null) {
+					inlineCallback.onNo(tag);
+				} else {
+					callback.onNo(tag);
 				}
 				}
 			});
 			});
 		}
 		}

+ 8 - 2
app/src/main/java/ch/threema/app/dialogs/ShowOnceDialog.java

@@ -24,7 +24,6 @@ package ch.threema.app.dialogs;
 import android.app.Activity;
 import android.app.Activity;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences;
 import android.os.Bundle;
 import android.os.Bundle;
-import android.preference.PreferenceManager;
 import android.view.View;
 import android.view.View;
 import android.widget.TextView;
 import android.widget.TextView;
 
 
@@ -36,6 +35,7 @@ import androidx.appcompat.app.AppCompatDialog;
 import androidx.appcompat.widget.AppCompatCheckBox;
 import androidx.appcompat.widget.AppCompatCheckBox;
 import androidx.fragment.app.FragmentManager;
 import androidx.fragment.app.FragmentManager;
 import androidx.fragment.app.FragmentTransaction;
 import androidx.fragment.app.FragmentTransaction;
+import androidx.preference.PreferenceManager;
 import ch.threema.app.R;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.ThreemaApplication;
 
 
@@ -47,7 +47,7 @@ import ch.threema.app.ThreemaApplication;
 public class ShowOnceDialog extends ThreemaDialogFragment {
 public class ShowOnceDialog extends ThreemaDialogFragment {
 	private AlertDialog alertDialog;
 	private AlertDialog alertDialog;
 	private Activity activity;
 	private Activity activity;
-	private static String PREF_PREFIX = "dialog_";
+	public static final String PREF_PREFIX = "dialog_";
 
 
 	public static ShowOnceDialog newInstance(@StringRes int title, @StringRes int message) {
 	public static ShowOnceDialog newInstance(@StringRes int title, @StringRes int message) {
 		ShowOnceDialog dialog = new ShowOnceDialog();
 		ShowOnceDialog dialog = new ShowOnceDialog();
@@ -78,6 +78,12 @@ public class ShowOnceDialog extends ThreemaDialogFragment {
 		}
 		}
 	}
 	}
 
 
+	// generally allow state loss for simple string alerts
+	public static boolean shouldNotShowAnymore(String tag) {
+		final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(ThreemaApplication.getAppContext());
+		return sharedPreferences.getBoolean(PREF_PREFIX + tag, false);
+	}
+
 	@Override
 	@Override
 	public AppCompatDialog onCreateDialog(Bundle savedInstanceState) {
 	public AppCompatDialog onCreateDialog(Bundle savedInstanceState) {
 		final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(ThreemaApplication.getAppContext());
 		final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(ThreemaApplication.getAppContext());

+ 2 - 5
app/src/main/java/ch/threema/app/dialogs/SimpleStringAlertDialog.java

@@ -27,7 +27,6 @@ import android.os.Bundle;
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 
 
 import androidx.annotation.NonNull;
 import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
 import androidx.appcompat.app.AppCompatDialog;
 import androidx.appcompat.app.AppCompatDialog;
 import androidx.fragment.app.FragmentManager;
 import androidx.fragment.app.FragmentManager;
 import androidx.fragment.app.FragmentTransaction;
 import androidx.fragment.app.FragmentTransaction;
@@ -35,8 +34,7 @@ import ch.threema.app.R;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.TestUtil;
 
 
 public class SimpleStringAlertDialog extends ThreemaDialogFragment {
 public class SimpleStringAlertDialog extends ThreemaDialogFragment {
-	private AlertDialog alertDialog;
-	private Activity activity;
+	protected Activity activity;
 
 
 	public static SimpleStringAlertDialog newInstance(int title, CharSequence message) {
 	public static SimpleStringAlertDialog newInstance(int title, CharSequence message) {
 		SimpleStringAlertDialog dialog = new SimpleStringAlertDialog();
 		SimpleStringAlertDialog dialog = new SimpleStringAlertDialog();
@@ -111,7 +109,6 @@ public class SimpleStringAlertDialog extends ThreemaDialogFragment {
 			builder.setMessage(message);
 			builder.setMessage(message);
 		}
 		}
 
 
-		alertDialog = builder.create();
-		return alertDialog;
+		return builder.create();
 	}
 	}
 }
 }

+ 2 - 7
app/src/main/java/ch/threema/app/dialogs/TextEntryDialog.java

@@ -24,7 +24,6 @@ package ch.threema.app.dialogs;
 import android.app.Activity;
 import android.app.Activity;
 import android.content.DialogInterface;
 import android.content.DialogInterface;
 import android.content.res.ColorStateList;
 import android.content.res.ColorStateList;
-import android.os.Build;
 import android.os.Bundle;
 import android.os.Bundle;
 import android.telephony.PhoneNumberFormattingTextWatcher;
 import android.telephony.PhoneNumberFormattingTextWatcher;
 import android.text.Editable;
 import android.text.Editable;
@@ -45,7 +44,7 @@ import ch.threema.app.emojis.EmojiEditText;
 import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.services.LocaleService;
 import ch.threema.app.services.LocaleService;
 import ch.threema.app.utils.DialogUtil;
 import ch.threema.app.utils.DialogUtil;
-import ch.threema.client.ProtocolDefines;
+import ch.threema.domain.protocol.csp.ProtocolDefines;
 
 
 public class TextEntryDialog extends ThreemaDialogFragment {
 public class TextEntryDialog extends ThreemaDialogFragment {
 	private TextEntryDialogClickListener callback;
 	private TextEntryDialogClickListener callback;
@@ -260,11 +259,7 @@ public class TextEntryDialog extends ThreemaDialogFragment {
 		if (inputFilterType == INPUT_FILTER_TYPE_IDENTITY) {
 		if (inputFilterType == INPUT_FILTER_TYPE_IDENTITY) {
 			editText.setFilters(new InputFilter[]{new InputFilter.AllCaps(), new InputFilter.LengthFilter(ProtocolDefines.IDENTITY_LEN)});
 			editText.setFilters(new InputFilter[]{new InputFilter.AllCaps(), new InputFilter.LengthFilter(ProtocolDefines.IDENTITY_LEN)});
 		} else if (inputFilterType == INPUT_FILTER_TYPE_PHONE && localeService != null) {
 		} else if (inputFilterType == INPUT_FILTER_TYPE_PHONE && localeService != null) {
-			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-				editText.addTextChangedListener(new PhoneNumberFormattingTextWatcher(localeService.getCountryIsoCode()));
-			} else {
-				editText.addTextChangedListener(new PhoneNumberFormattingTextWatcher());
-			}
+			editText.addTextChangedListener(new PhoneNumberFormattingTextWatcher(localeService.getCountryIsoCode()));
 			editText.addTextChangedListener(new TextWatcher() {
 			editText.addTextChangedListener(new TextWatcher() {
 				@Override
 				@Override
 				public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
 				public void beforeTextChanged(CharSequence s, int start, int count, int after) {}

+ 43 - 1
app/src/main/java/ch/threema/app/dialogs/WizardDialog.java

@@ -31,6 +31,7 @@ import android.widget.TextView;
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 
 
 import androidx.appcompat.app.AppCompatDialog;
 import androidx.appcompat.app.AppCompatDialog;
+import androidx.core.content.res.ResourcesCompat;
 import ch.threema.app.R;
 import ch.threema.app.R;
 
 
 public class WizardDialog extends ThreemaDialogFragment {
 public class WizardDialog extends ThreemaDialogFragment {
@@ -38,16 +39,24 @@ public class WizardDialog extends ThreemaDialogFragment {
 	private static final String ARG_TITLE_STRING = "titleString";
 	private static final String ARG_TITLE_STRING = "titleString";
 	private static final String ARG_POSITIVE = "positive";
 	private static final String ARG_POSITIVE = "positive";
 	private static final String ARG_NEGATIVE = "negative";
 	private static final String ARG_NEGATIVE = "negative";
+	private static final String ARG_HIGHLIGHT = "highlight";
 
 
 	private WizardDialogCallback callback;
 	private WizardDialogCallback callback;
 	private Activity activity;
 	private Activity activity;
+	public enum Highlight {
+		POSITIVE,
+		NEGATIVE,
+		EQUAL,
+		NONE
+	}
 
 
-	public static WizardDialog newInstance(int title, int positive, int negative) {
+	public static WizardDialog newInstance(int title, int positive, int negative, Highlight highlight) {
 		WizardDialog dialog = new WizardDialog();
 		WizardDialog dialog = new WizardDialog();
 		Bundle args = new Bundle();
 		Bundle args = new Bundle();
 		args.putInt(ARG_TITLE, title);
 		args.putInt(ARG_TITLE, title);
 		args.putInt(ARG_POSITIVE, positive);
 		args.putInt(ARG_POSITIVE, positive);
 		args.putInt(ARG_NEGATIVE, negative);
 		args.putInt(ARG_NEGATIVE, negative);
+		args.putSerializable(ARG_HIGHLIGHT, highlight);
 		dialog.setArguments(args);
 		dialog.setArguments(args);
 		return dialog;
 		return dialog;
 	}
 	}
@@ -107,6 +116,7 @@ public class WizardDialog extends ThreemaDialogFragment {
 		String titleString = getArguments().getString(ARG_TITLE_STRING);
 		String titleString = getArguments().getString(ARG_TITLE_STRING);
 		int positive = getArguments().getInt(ARG_POSITIVE);
 		int positive = getArguments().getInt(ARG_POSITIVE);
 		int negative = getArguments().getInt(ARG_NEGATIVE, 0);
 		int negative = getArguments().getInt(ARG_NEGATIVE, 0);
+		Highlight highlight = (Highlight) getArguments().getSerializable(ARG_HIGHLIGHT);
 		final String tag = this.getTag();
 		final String tag = this.getTag();
 
 
 		final View dialogView = activity.getLayoutInflater().inflate(R.layout.dialog_wizard, null);
 		final View dialogView = activity.getLayoutInflater().inflate(R.layout.dialog_wizard, null);
@@ -142,11 +152,43 @@ public class WizardDialog extends ThreemaDialogFragment {
 			negativeButton.setVisibility(View.GONE);
 			negativeButton.setVisibility(View.GONE);
 		}
 		}
 
 
+		if (highlight != null) {
+			switch (highlight) {
+				case NONE:
+					hightlightButton(negativeButton, false);
+					hightlightButton(positiveButton, false);
+				case EQUAL:
+					hightlightButton(negativeButton, true);
+					hightlightButton(positiveButton, true);
+					break;
+				case NEGATIVE:
+					hightlightButton(negativeButton, true);
+					hightlightButton(positiveButton, false);
+					break;
+				case POSITIVE:
+				default:
+					positiveButton.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.selector_button_green, null));
+					positiveButton.setTextColor(getResources().getColor(R.color.wizard_button_text_inverse));
+					break;
+			}
+		}
+
 		setCancelable(false);
 		setCancelable(false);
 
 
 		return builder.create();
 		return builder.create();
 	}
 	}
 
 
+	private void hightlightButton(Button button, boolean hightlight) {
+		if (hightlight) {
+			button.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.selector_button_green, null));
+			button.setTextColor(getResources().getColor(R.color.wizard_button_text_inverse));
+		}
+		else {
+			button.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.selector_button_green_inverse, null));
+			button.setTextColor(getResources().getColor(R.color.wizard_button_text));
+		}
+	}
+
 	@Override
 	@Override
 	public void onCancel(DialogInterface dialogInterface) {
 	public void onCancel(DialogInterface dialogInterface) {
 		callback.onNo(this.getTag());
 		callback.onNo(this.getTag());

部分文件因文件數量過多而無法顯示