Threema 4 лет назад
Родитель
Сommit
97957c97e0
100 измененных файлов с 2475 добавлено и 1341 удалено
  1. 16 0
      README.md
  2. 15 0
      THANKS.md
  3. 157 44
      app/build.gradle
  4. BIN
      app/libs/agconnect-core-1.4.0.300.aar
  5. BIN
      app/libs/agconnect-crash-symbol-lib-1.4.2.300.jar
  6. BIN
      app/libs/agcp-1.4.2.300.jar
  7. 7 0
      app/proguard-project.txt
  8. 1 4
      app/src/androidTest/java/ch/threema/app/voip/SdpTest.java
  9. 101 0
      app/src/google_services_based/java/ch/threema/app/push/PushRegistrationWorker.java
  10. 119 0
      app/src/google_services_based/java/ch/threema/app/push/PushService.java
  11. 179 0
      app/src/google_services_based/java/ch/threema/app/wearable/WearableHandler.java
  12. 23 0
      app/src/hms/AndroidManifest.xml
  13. 39 0
      app/src/hms/agconnect-services.json
  14. 29 0
      app/src/hms/java/ch/threema/app/activities/DownloadApkActivity.java
  15. 26 0
      app/src/hms/java/ch/threema/app/utils/DownloadUtil.java
  16. 105 0
      app/src/hms_services_based/java/ch/threema/app/push/PushRegistrationWorker.java
  17. 118 0
      app/src/hms_services_based/java/ch/threema/app/push/PushService.java
  18. 40 0
      app/src/hms_services_based/java/ch/threema/app/wearable/WearableHandler.java
  19. 23 0
      app/src/hms_work/AndroidManifest.xml
  20. 39 0
      app/src/hms_work/agconnect-services.json
  21. 29 0
      app/src/hms_work/java/ch/threema/app/activities/DownloadApkActivity.java
  22. 26 0
      app/src/hms_work/java/ch/threema/app/utils/DownloadUtil.java
  23. 1 41
      app/src/main/AndroidManifest.xml
  24. 11 1
      app/src/main/java/ch/threema/app/BuildFlavor.java
  25. 0 418
      app/src/main/java/ch/threema/app/FcmListenerService.java
  26. 0 148
      app/src/main/java/ch/threema/app/FcmRegistrationIntentService.java
  27. 5 4
      app/src/main/java/ch/threema/app/ThreemaApplication.java
  28. 17 10
      app/src/main/java/ch/threema/app/activities/DirectoryActivity.java
  29. 8 1
      app/src/main/java/ch/threema/app/activities/EnterSerialActivity.java
  30. 7 4
      app/src/main/java/ch/threema/app/activities/HomeActivity.java
  31. 11 3
      app/src/main/java/ch/threema/app/activities/ImagePaintActivity.java
  32. 48 3
      app/src/main/java/ch/threema/app/activities/MediaGalleryActivity.java
  33. 2 2
      app/src/main/java/ch/threema/app/activities/SendMediaActivity.java
  34. 32 24
      app/src/main/java/ch/threema/app/activities/wizard/WizardStartActivity.java
  35. 4 0
      app/src/main/java/ch/threema/app/adapters/ComposeMessageAdapter.java
  36. 0 2
      app/src/main/java/ch/threema/app/adapters/ContactListAdapter.java
  37. 17 10
      app/src/main/java/ch/threema/app/adapters/DirectoryAdapter.java
  38. 7 9
      app/src/main/java/ch/threema/app/adapters/MediaGalleryAdapter.java
  39. 1 5
      app/src/main/java/ch/threema/app/adapters/decorators/FirstUnreadChatAdapterDecorator.java
  40. 2 13
      app/src/main/java/ch/threema/app/asynctasks/DeleteIdentityAsyncTask.java
  41. 8 1
      app/src/main/java/ch/threema/app/backuprestore/csv/BackupService.java
  42. 2 0
      app/src/main/java/ch/threema/app/backuprestore/csv/RestoreService.java
  43. 14 9
      app/src/main/java/ch/threema/app/camera/CameraFragment.java
  44. 6 0
      app/src/main/java/ch/threema/app/camera/CameraView.java
  45. 13 5
      app/src/main/java/ch/threema/app/camera/CameraXModule.java
  46. 3 0
      app/src/main/java/ch/threema/app/dialogs/MessageDetailDialog.java
  47. 30 4
      app/src/main/java/ch/threema/app/fragments/BackupDataFragment.java
  48. 9 1
      app/src/main/java/ch/threema/app/fragments/ComposeMessageFragment.java
  49. 2 3
      app/src/main/java/ch/threema/app/fragments/MessageSectionFragment.java
  50. 0 53
      app/src/main/java/ch/threema/app/jobs/FcmRegistrationJobService.java
  51. BIN
      app/src/main/java/ch/threema/app/libs/agcp-1.4.2.300.jar
  52. BIN
      app/src/main/java/ch/threema/app/libs/push-5.0.4.302.aar
  53. 4 7
      app/src/main/java/ch/threema/app/managers/ServiceManager.java
  54. 3 3
      app/src/main/java/ch/threema/app/preference/SettingsAboutFragment.java
  55. 42 42
      app/src/main/java/ch/threema/app/preference/SettingsTroubleshootingFragment.java
  56. 3 0
      app/src/main/java/ch/threema/app/processors/MessageProcessor.java
  57. 3 0
      app/src/main/java/ch/threema/app/receivers/AcknowledgeActionBroadcastReceiver.java
  58. 2 0
      app/src/main/java/ch/threema/app/receivers/DeclineActionBroadcastReceiver.java
  59. 3 0
      app/src/main/java/ch/threema/app/receivers/MarkReadActionBroadcastReceiver.java
  60. 2 3
      app/src/main/java/ch/threema/app/receivers/ReSendMessagesBroadcastReceiver.java
  61. 2 0
      app/src/main/java/ch/threema/app/receivers/ReplyActionBroadcastReceiver.java
  62. 6 1
      app/src/main/java/ch/threema/app/receivers/UpdateReceiver.java
  63. 37 4
      app/src/main/java/ch/threema/app/routines/CheckLicenseRoutine.java
  64. 1 1
      app/src/main/java/ch/threema/app/services/AvatarCacheServiceImpl.java
  65. 4 4
      app/src/main/java/ch/threema/app/services/ContactServiceImpl.java
  66. 9 8
      app/src/main/java/ch/threema/app/services/ConversationTagService.java
  67. 24 33
      app/src/main/java/ch/threema/app/services/ConversationTagServiceImpl.java
  68. 2 1
      app/src/main/java/ch/threema/app/services/FileService.java
  69. 9 3
      app/src/main/java/ch/threema/app/services/FileServiceImpl.java
  70. 4 1
      app/src/main/java/ch/threema/app/services/MessageService.java
  71. 67 13
      app/src/main/java/ch/threema/app/services/MessageServiceImpl.java
  72. 11 7
      app/src/main/java/ch/threema/app/services/NotificationServiceImpl.java
  73. 2 2
      app/src/main/java/ch/threema/app/services/PreferenceServiceImpl.java
  74. 66 23
      app/src/main/java/ch/threema/app/services/UserServiceImpl.java
  75. 1 0
      app/src/main/java/ch/threema/app/services/messageplayer/AudioMessagePlayer.java
  76. 11 0
      app/src/main/java/ch/threema/app/services/messageplayer/MessagePlayer.java
  77. 6 0
      app/src/main/java/ch/threema/app/services/messageplayer/WebClientMessagePlayer.java
  78. 79 0
      app/src/main/java/ch/threema/app/ui/FastScrollGridView.java
  79. 0 67
      app/src/main/java/ch/threema/app/ui/OnKeyboardBackRespondingSearchView.java
  80. 1 2
      app/src/main/java/ch/threema/app/utils/AndroidContactUtil.java
  81. 9 11
      app/src/main/java/ch/threema/app/utils/ConfigUtils.java
  82. 9 2
      app/src/main/java/ch/threema/app/utils/ConversationNotificationUtil.java
  83. 11 6
      app/src/main/java/ch/threema/app/utils/FileUtil.java
  84. 33 4
      app/src/main/java/ch/threema/app/utils/MessageUtil.java
  85. 381 40
      app/src/main/java/ch/threema/app/utils/PushUtil.java
  86. 2 0
      app/src/main/java/ch/threema/app/utils/StateBitmapUtil.java
  87. 20 0
      app/src/main/java/ch/threema/app/voip/CallState.java
  88. 12 14
      app/src/main/java/ch/threema/app/voip/PeerConnectionClient.java
  89. 4 2
      app/src/main/java/ch/threema/app/voip/activities/CallActivity.java
  90. 11 9
      app/src/main/java/ch/threema/app/voip/services/VoipCallService.java
  91. 35 143
      app/src/main/java/ch/threema/app/voip/services/VoipStateService.java
  92. 15 3
      app/src/main/java/ch/threema/app/webclient/converter/ClientInfo.java
  93. 4 1
      app/src/main/java/ch/threema/app/webclient/converter/MessageState.java
  94. 62 52
      app/src/main/java/ch/threema/client/APIConnector.java
  95. 8 0
      app/src/main/java/ch/threema/client/CreateIdentityRequestDataInterface.java
  96. 2 0
      app/src/main/java/ch/threema/client/ProtocolDefines.java
  97. 1 1
      app/src/main/java/ch/threema/client/ProtocolStrings.java
  98. 2 1
      app/src/main/java/ch/threema/storage/models/MessageState.java
  99. 6 5
      app/src/main/java/ch/threema/storage/models/TagModel.java
  100. 92 0
      app/src/main/java/com/DrmSDK/Constants.java

+ 16 - 0
README.md

@@ -68,6 +68,20 @@ backup can then be imported into the self-compiled app.
 
 Note that the ID creation endpoint is monitored for abuse.
 
+### Huawei HMS Licensing
+
+When creating a new Threema ID using the Threema app bought on Huawei AppGallery, the
+app sends a [Huawei DRM Signature](https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/appgallerykit-paidapps-devguide-0000001073913394)
+to the directory server. This allows the server to verify that you have indeed
+bought the app, without being able to identify you.
+
+This means that a self-compiled app using the `hms` build variant cannot be
+used to create a new Threema ID. You can, however, use an app that was
+purchased over Huawei AppGallery to create an ID and then export a backup. This
+backup can then be imported into the self-compiled app.
+
+Note that the ID creation endpoint is monitored for abuse.
+
 ### Threema Shop Licensing
 
 If you bought a Threema for Android license in the [Threema Shop](https://shop.threema.ch/),
@@ -97,6 +111,8 @@ There are currently six product flavors:
 | `store_google`      | Google Play Store version (regular, paid app) | Google Play    |
 | `store_google_work` | Google Play Store version (work, free app)    | Threema Work   |
 | `store_threema`     | Threema Store version                         | Threema Shop   |
+| `hms`               | Huawei AppGallery version (regular, paid app) | Huawei HMS     |
+| `hms_work`          | Huawei AppGallery version (work, free app)    | Threema Work   |
 | `sandbox`           | Uses sandbox test environment¹                | Allowlist      |
 | `sandbox_work`      | Uses sandbox test environment¹                | Threema Work   |
 | `red`               | Uses sandbox test environment¹                | Threema Work   |

+ 15 - 0
THANKS.md

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

+ 157 - 44
app/build.gradle

@@ -4,6 +4,12 @@ plugins {
 
 apply plugin: 'com.android.application'
 
+// only apply the plugin if we are dealing with a AppGallery build
+if (getGradle().getStartParameter().getTaskRequests().toString().contains("Hms")) {
+    println "enabling hms plugin"
+    apply plugin: 'com.huawei.agconnect'
+}
+
 /**
  * Return the git hash, if git is installed.
  */
@@ -60,6 +66,7 @@ def findKeystore = { name ->
 def keystores = [
     debug: findKeystore("debug"),
     release: findKeystore("threema"),
+    hms_release: findKeystore("threema_hms"),
 ]
 
 android {
@@ -75,8 +82,8 @@ android {
         vectorDrawables.useSupportLibrary = true
         applicationId "ch.threema.app"
         testApplicationId 'ch.threema.app.test'
-        versionCode 675
-        versionName "4.53"
+        versionCode 679
+        versionName "4.54"
         resValue "string", "version_name_suffix", ""
         resValue "string", "app_name", "Threema"
         resValue "string", "uri_scheme", "threema"
@@ -98,6 +105,7 @@ 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_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 "boolean", "SEND_CONSUMED_DELIVERY_RECEIPTS", "false"
 
         manifestPlaceholders = [
             actionUrl: "go.threema.ch",
@@ -141,7 +149,7 @@ android {
         }
         store_threema { }
         store_google_work {
-            versionName "4.53k"
+            versionName "4.54k"
             applicationId "ch.threema.app.work"
             testApplicationId 'ch.threema.app.work.test'
             resValue "string", "package_name", applicationId
@@ -178,7 +186,7 @@ android {
             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 }"
         }
         sandbox_work {
-            versionName "4.53k"
+            versionName "4.54k"
             applicationId "ch.threema.app.sandbox.work"
             testApplicationId 'ch.threema.app.sandbox.work.test'
 
@@ -208,7 +216,7 @@ android {
             ]
         }
         red { // Essentially like sandbox work, but with a different icon and accent color, used for internal testing
-            versionName "4.53r"
+            versionName "4.54r"
             applicationId "ch.threema.app.red"
             testApplicationId 'ch.threema.app.red.test'
 
@@ -230,6 +238,36 @@ android {
             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_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 "boolean", "SEND_CONSUMED_DELIVERY_RECEIPTS", "true"
+
+            manifestPlaceholders = [
+                actionUrl: "work.threema.ch",
+                uriScheme: "threemawork",
+                contactActionUrl: "threema.id"
+            ]
+        }
+        hms {
+            applicationId "ch.threema.app.hms"
+            resValue "string", "package_name", applicationId
+        }
+        hms_work {
+            versionName "4.54k"
+            applicationId "ch.threema.app.work.hms"
+            testApplicationId 'ch.threema.app.work.test.hms'
+            resValue "string", "package_name", applicationId
+            resValue "string", "app_name", "Threema Work"
+            resValue "string", "uri_scheme", "threemawork"
+            resValue "string", "action_url", "work.threema.ch"
+            resValue "string", "contact_action_url", "threema.id"
+            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 "integer", "max_group_size", "256"
+            resValue "string", "shop_download_filename", ""
+            buildConfigField "String", "CHAT_SERVER_PREFIX", "\"w-\""
+            buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"ds.\""
+            buildConfigField "String", "CHAT_SERVER_SUFFIX", "\".0.threema.ch\""
+            buildConfigField "String", "MEDIA_PATH", "\"ThreemaWork\""
+            buildConfigField "boolean", "CHAT_SERVER_GROUPS", "true"
 
             manifestPlaceholders = [
                 actionUrl: "work.threema.ch",
@@ -260,23 +298,57 @@ android {
         } else {
             logger.warn("No release keystore found. Falling back to locally generated keystore.")
         }
+
+        // Release config
+        if (keystores.hms_release != null) {
+            hms_release {
+                storeFile file(keystores.hms_release.storeFile)
+                storePassword keystores.hms_release.storePassword
+                keyAlias keystores.hms_release.keyAlias
+                keyPassword keystores.hms_release.keyPassword
+            }
+        } else {
+            logger.warn("No hms keystore found. Falling back to locally generated keystore.")
+        }
     }
 
     sourceSets {
+        none {
+            java.srcDir 'src/google_services_based/java'
+            manifest.srcFile 'src/store_google/AndroidManifest.xml'
+        }
         main {
             assets.srcDirs = ['assets']
             jniLibs.srcDirs = ['libs']
         }
+        store_google{
+            java.srcDir 'src/google_services_based/java'
+        }
+        store_google_work {
+            java.srcDir 'src/google_services_based/java'
+        }
+        store_threema {
+            java.srcDir 'src/google_services_based/java'
+        }
+        hms {
+            java.srcDir 'src/hms_services_based/java'
+        }
+        hms_work {
+            java.srcDir 'src/hms_services_based/java'
+            res.srcDir 'src/store_google_work/res'
+        }
         sandbox {
-            java {
-                srcDir 'src/none'
-            }
+            java.srcDir 'src/google_services_based/java'
+            manifest.srcFile 'src/store_google/AndroidManifest.xml'
         }
         sandbox_work {
-            setRoot 'src/store_google_work'
+            java.srcDir 'src/google_services_based/java'
+            res.srcDir 'src/store_google_work/res'
+            manifest.srcFile 'src/store_google_work/AndroidManifest.xml'
         }
         red {
-            setRoot 'src/red'
+            java.srcDir 'src/google_services_based/java'
+            res.srcDir 'src/red/res'
         }
     }
 
@@ -286,6 +358,7 @@ android {
             jniDebuggable false
             multiDexEnabled true
             multiDexKeepProguard file('multidex-keep.pro')
+
             if (keystores['debug'] != null) {
                 signingConfig signingConfigs.debug
             }
@@ -299,8 +372,20 @@ android {
             multiDexEnabled true
             multiDexKeepProguard file('multidex-keep.pro')
             proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-project.txt'
+
             if (keystores['release'] != null) {
-                signingConfig signingConfigs.release
+                productFlavors.store_google.signingConfig signingConfigs.release
+                productFlavors.store_google_work.signingConfig signingConfigs.release
+                productFlavors.store_threema.signingConfig signingConfigs.release
+                productFlavors.red.signingConfig signingConfigs.release
+                productFlavors.sandbox.signingConfig signingConfigs.release
+                productFlavors.sandbox_work.signingConfig signingConfigs.release
+                productFlavors.none.signingConfig signingConfigs.release
+            }
+
+            if (keystores['hms_release'] != null) {
+                productFlavors.hms.signingConfig signingConfigs.hms_release
+                productFlavors.hms_work.signingConfig signingConfigs.hms_release
             }
         }
     }
@@ -411,19 +496,6 @@ dependencies {
         //resolutionStrategy.failOnVersionConflict()
     }
 
-    // Include all JARs in the "libs" directory
-    implementation fileTree(dir: 'libs', include: ['*.jar'])
-
-    // play services
-    implementation 'com.google.android.gms:play-services-base:16.1.0'
-    implementation 'com.google.android.gms:play-services-wearable:16.0.1'
-    // do not upgrade to a higher version of firebase-messaging - as we do not want the Firebase Installations API in our app
-    implementation('com.google.firebase:firebase-messaging:20.1.0') {
-        exclude group: 'com.google.firebase', module: 'firebase-core'
-        exclude group: 'com.google.firebase', module: 'firebase-analytics'
-        exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
-    }
-
     implementation 'net.zetetic:android-database-sqlcipher:4.4.2'
 
     implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
@@ -431,10 +503,9 @@ dependencies {
     implementation 'net.lingala.zip4j:zip4j:2.7.0'
     implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.2'
     implementation 'com.mapbox.mapboxsdk:mapbox-android-sdk:9.2.1'
-    // commons-io 2.6 requires android 4.4
-    implementation 'commons-io:commons-io:2.4'
-    implementation 'org.slf4j:slf4j-api:1.7.25'
-    implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.21'
+    implementation 'commons-io:commons-io:2.8.0'
+    implementation 'org.slf4j:slf4j-api:1.7.30'
+    implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.23'
     implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
     implementation 'com.datatheorem.android.trustkit:trustkit:1.1.3'
     implementation 'com.takisoft.preferencex:preferencex:1.1.0'
@@ -451,12 +522,12 @@ dependencies {
     implementation 'androidx.biometric:biometric:1.1.0'
     implementation "androidx.work:work-runtime:2.5.0"
     implementation 'androidx.fragment:fragment:1.3.3'
-    implementation 'androidx.activity:activity:1.2.2'
+    implementation 'androidx.activity:activity:1.2.3'
     implementation 'androidx.sqlite:sqlite:2.1.0'
     implementation "androidx.concurrent:concurrent-futures:1.1.0"
-    implementation "androidx.camera:camera-camera2:1.0.0-rc05"
-    implementation "androidx.camera:camera-lifecycle:1.0.0-rc05"
-    implementation "androidx.camera:camera-view:1.0.0-alpha23"
+    implementation "androidx.camera:camera-camera2:1.0.0"
+    implementation "androidx.camera:camera-lifecycle:1.0.0"
+    implementation "androidx.camera:camera-view:1.0.0-alpha24"
     implementation 'androidx.multidex:multidex:2.0.1'
     implementation "androidx.lifecycle:lifecycle-viewmodel:2.3.1"
     implementation "androidx.lifecycle:lifecycle-livedata:2.3.1"
@@ -467,17 +538,17 @@ dependencies {
     implementation "androidx.lifecycle:lifecycle-common-java8:2.3.1"
     implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
     implementation 'androidx.legacy:legacy-support-v4:1.0.0'
-    implementation "androidx.paging:paging-runtime:2.1.2"
+    implementation "androidx.paging:paging-runtime:3.0.0"
 
     implementation 'com.google.android.material:material:1.3.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.zxing:core:3.3.3' // zxing 3.4 crashes on kitkat
-    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.20'
+    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.22'
 
     // webclient dependencies
-    implementation 'org.msgpack:msgpack-core:0.8.20'
+    implementation 'org.msgpack:msgpack-core:0.8.22!!'
     implementation 'com.neovisionaries:nv-websocket-client:2.9'
 
     // Backport of Streams and CompletableFuture. Remove once API level 24 is supported.
@@ -486,21 +557,16 @@ dependencies {
     // Google Assistant Voice Action verification library
     implementation(name:'libgsaverification-client', ext:'aar')
 
-    implementation('org.saltyrtc.client:saltyrtc-client:0.14.1') {
+    implementation('org.saltyrtc:saltyrtc-client:0.14.2') {
         exclude group: 'org.json'
     }
 
-    implementation 'org.saltyrtc.chunked-dc:chunked-dc:1.0.0'
-    implementation 'ch.threema.webrtc:webrtc-android:84.2.0'
-    implementation('org.saltyrtc.tasks.webrtc:saltyrtc-task-webrtc:0.18.0') {
+    implementation 'org.saltyrtc:chunked-dc:1.0.1'
+    implementation 'ch.threema:webrtc-android:91.0.1'
+    implementation('org.saltyrtc:saltyrtc-task-webrtc:0.18.1') {
         exclude module: 'saltyrtc-client'
     }
 
-    // Room components
-    implementation 'androidx.room:room-runtime:2.2.6'
-    annotationProcessor 'androidx.room:room-compiler:2.2.6'
-    androidTestImplementation 'androidx.room:room-testing:2.2.6'
-
     // Glide components
     implementation 'com.github.bumptech.glide:glide:4.11.0'
     annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
@@ -543,6 +609,53 @@ dependencies {
         exclude group: 'androidx.annotation', module: 'annotation'
     }
     androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
+
+    // Google Play Services and related libraries
+    def googleDependencies = [
+        // Play services
+        'com.google.android.gms:play-services-base:16.1.0': [],
+
+        // Support for wearables
+        'com.google.android.gms:play-services-wearable:17.0.0': [],
+
+        // Firebase push
+        //
+        // Note: Do not upgrade to a higher version of firebase-messaging,
+        //       as we do not want the Firebase Installations API in our app
+        'com.google.firebase:firebase-messaging:20.1.0': [
+            [group: 'com.google.firebase', module: 'firebase-core'],
+            [group: 'com.google.firebase', module: 'firebase-analytics'],
+            [group: 'com.google.firebase', module: 'firebase-measurement-connector'],
+        ],
+    ]
+    googleDependencies.each {
+        def dependency = it.key
+        def excludes = it.value
+        noneImplementation(dependency) { excludes.each { exclude it } }
+        store_googleImplementation(dependency) { excludes.each { exclude it } }
+        store_google_workImplementation(dependency) { excludes.each { exclude it } }
+        store_threemaImplementation(dependency) { excludes.each { exclude it } }
+        sandboxImplementation(dependency) { excludes.each { exclude it } }
+        sandbox_workImplementation(dependency) { excludes.each { exclude it } }
+        redImplementation(dependency) { excludes.each { exclude it } }
+    }
+
+    // Huawei related libraries (only for hms* build variants)
+    def huaweiDependencies = [
+        // HMS push
+        'com.huawei.hms:push:5.0.4.302': [
+            // Exclude agconnect dependency, we'll replace it with the vendored version below
+            [group: 'com.huawei.agconnect'],
+        ],
+    ]
+    huaweiDependencies.each {
+        def dependency = it.key
+        def excludes = it.value
+        hmsImplementation(dependency) { excludes.each { exclude it } }
+        hms_workImplementation(dependency) { excludes.each { exclude it } }
+    }
+    hmsImplementation(name: 'agconnect-core-1.4.0.300', ext: 'aar')
+    hms_workImplementation(name: 'agconnect-core-1.4.0.300', ext: 'aar')
 }
 
 sonarqube {

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


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


BIN
app/libs/agcp-1.4.2.300.jar


+ 7 - 0
app/proguard-project.txt

@@ -139,6 +139,13 @@ public static <fields>;
 -keepattributes EnclosingMethod
 -keepattributes InnerClasses
 
+# hms requirements
+-ignorewarnings
+-keepattributes Exceptions
+-keep class com.huawei.updatesdk.**{*;}
+-keep class com.huawei.hms.**{*;}
+-keep class com.huawei.android.sdk.drm.**{*;}
+
 # remove android log calls below error
 -assumenosideeffects class android.util.Log {
     public static *** v(...);

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

@@ -429,9 +429,6 @@ public class SdpTest {
 		if (isOffer || videoEnabled) {
 			expectedMatchesPart2.add("^m=application 9 UDP/DTLS/SCTP webrtc-datachannel$");
 			expectedMatchesPart2.add("^c=IN IP4 0.0.0.0$");
-			if (!isOffer) {
-				expectedMatchesPart2.add("^b=AS:30$");
-			}
 			expectedMatchesPart2.add("^a=ice-ufrag:[^ ]+$");
 			expectedMatchesPart2.add("^a=ice-pwd:[^ ]+$");
 			expectedMatchesPart2.add("^a=ice-options:trickle renomination$");
@@ -484,7 +481,7 @@ public class SdpTest {
 			final String actual = i < actualLines.size() ? actualLines.get(i + offset) : null;
 			Log.d(TAG, "Validating \"" + actual + "\" against \"" + expected + "\"");
 			assertNotNull(actual);
-			assertTrue(actual.matches(expected));
+			assertTrue("Line \"" + actual + "\" did not match \"" + expected + "\"", actual.matches(expected));
 		}
 	}
 

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

@@ -0,0 +1,101 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * 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.push;
+
+import android.content.Context;
+
+import com.google.firebase.iid.FirebaseInstanceId;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+import androidx.annotation.NonNull;
+import androidx.work.Data;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+import ch.threema.app.utils.PushUtil;
+import ch.threema.base.ThreemaException;
+import ch.threema.client.ProtocolDefines;
+
+public class PushRegistrationWorker extends Worker {
+	private final Logger logger = LoggerFactory.getLogger(PushRegistrationWorker.class);
+
+	private final Context appContext;
+
+	/**
+	 * Constructor for the PushRegistrationWorker.
+	 *
+	 * Note: This constructor is called by the WorkManager, so don't add additional parameters!
+	 */
+	public PushRegistrationWorker(@NonNull Context appContext, @NonNull WorkerParameters workerParams) {
+		super(appContext, workerParams);
+		this.appContext = appContext;
+	}
+
+	@NonNull
+	@Override
+	public Result doWork() {
+		Data workerFlags = getInputData();
+		final boolean clearToken = workerFlags.getBoolean(PushService.EXTRA_CLEAR_TOKEN, false);
+		final boolean withCallback = workerFlags.getBoolean(PushService.EXTRA_WITH_CALLBACK, false);
+		logger.debug("doWork FCM registration clear {} withCallback {}", clearToken, withCallback);
+
+		if (clearToken) {
+			String error = null;
+			try {
+				FirebaseInstanceId.getInstance().deleteInstanceId();
+				PushUtil.sendTokenToServer(appContext, "", ProtocolDefines.PUSHTOKEN_TYPE_NONE);
+			} catch (IOException | ThreemaException e) {
+				logger.error("Exception", e);
+				error = e.getMessage();
+			}
+
+			if (withCallback) {
+				PushUtil.signalRegistrationFinished(error, true);
+			}
+		} else {
+			FirebaseInstanceId.getInstance().getInstanceId()
+				.addOnSuccessListener(instanceIdResult -> {
+					String token = instanceIdResult.getToken();
+					logger.info("Received FCM registration token");
+					String error = null;
+					try {
+						PushUtil.sendTokenToServer(appContext, token, ProtocolDefines.PUSHTOKEN_TYPE_GCM);
+					} catch (ThreemaException e) {
+						logger.error("Exception", e);
+						error = e.getMessage();
+					}
+					if (withCallback) {
+						PushUtil.signalRegistrationFinished(error, clearToken);
+					}
+				}).addOnFailureListener(e -> {
+				if (withCallback) {
+					PushUtil.signalRegistrationFinished(e.getMessage(), clearToken);
+				}
+			});
+		}
+		// required by the Worker interface but is not used for any error handling in the push registration process
+		return Result.success();
+	}
+}

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

@@ -0,0 +1,119 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2015-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.push;
+
+import android.content.Context;
+import android.text.format.DateUtils;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GoogleApiAvailability;
+import com.google.firebase.iid.FirebaseInstanceId;
+import com.google.firebase.messaging.FirebaseMessagingService;
+import com.google.firebase.messaging.RemoteMessage;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.Map;
+
+import androidx.annotation.NonNull;
+import ch.threema.app.utils.PushUtil;
+import ch.threema.app.utils.RuntimeUtil;
+import ch.threema.base.ThreemaException;
+import ch.threema.client.ProtocolDefines;
+
+public class PushService extends FirebaseMessagingService {
+	private static final Logger logger = LoggerFactory.getLogger(PushService.class);
+
+	public static final String EXTRA_CLEAR_TOKEN = "clear";
+	public static final String EXTRA_WITH_CALLBACK = "cb";
+
+	@Override
+	public void onNewToken(@NonNull String token) {
+		logger.info("New FCM token received");
+		try {
+			PushUtil.sendTokenToServer(this, token, ProtocolDefines.PUSHTOKEN_TYPE_GCM);
+		} catch (ThreemaException e) {
+			logger.error("onNewToken, could not send token to server ", e);
+		}
+	}
+
+	public static void deleteToken(Context context) {
+		try {
+			FirebaseInstanceId.getInstance().deleteInstanceId();
+			PushUtil.sendTokenToServer(context,"", ProtocolDefines.PUSHTOKEN_TYPE_NONE);
+		} catch (IOException | ThreemaException e) {
+			logger.warn("Could not delete FCM token", e);
+		}
+	}
+
+	@Override
+	public void onMessageReceived(@NonNull RemoteMessage remoteMessage) {
+		logger.info("Handling incoming FCM intent.");
+
+		RuntimeUtil.runInWakelock(getApplicationContext(), DateUtils.MINUTE_IN_MILLIS * 10, "PushService", () -> processFcmMessage(remoteMessage));
+	}
+
+	private void processFcmMessage(RemoteMessage remoteMessage) {
+		logger.info("Received FCM message: {}", remoteMessage.getMessageId());
+
+		// Log message sent time
+		try {
+			Date sentDate = new Date(remoteMessage.getSentTime());
+
+			logger.info("*** Message sent     : " + sentDate.toString(), true);
+			logger.info("*** Message received : " + new Date().toString(), true);
+			logger.info("*** Original priority: " + remoteMessage.getOriginalPriority());
+			logger.info("*** Current priority: " + remoteMessage.getPriority());
+		} catch (Exception ignore) {
+		}
+
+		Map<String, String> data = remoteMessage.getData();
+		PushUtil.processRemoteMessage(data);
+	}
+
+	// following services check are handled here and not in ConfigUtils to minimize number of duplicating classes
+	/**
+	 * check for specific huawei services
+	 */
+	public static boolean hmsServicesInstalled(Context context) {
+		return false;
+	}
+
+	/**
+	 * check for specific google services
+	 */
+	public static boolean playServicesInstalled(Context context) {
+		GoogleApiAvailability apiAvailability = com.google.android.gms.common.GoogleApiAvailability.getInstance();
+		int resultCode = apiAvailability.isGooglePlayServicesAvailable(context);
+		return RuntimeUtil.isInTest() || (resultCode == ConnectionResult.SUCCESS);
+	}
+
+	/**
+	 * check for available push service
+	 */
+	public static boolean servicesInstalled(Context context) {
+		return playServicesInstalled(context) || hmsServicesInstalled(context);
+	}
+}

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

@@ -0,0 +1,179 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * 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.wearable;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+
+import com.google.android.gms.common.data.FreezableUtils;
+import com.google.android.gms.tasks.Tasks;
+import com.google.android.gms.wearable.DataClient;
+import com.google.android.gms.wearable.DataEvent;
+import com.google.android.gms.wearable.DataEventBuffer;
+import com.google.android.gms.wearable.DataMapItem;
+import com.google.android.gms.wearable.Node;
+import com.google.android.gms.wearable.PutDataMapRequest;
+import com.google.android.gms.wearable.PutDataRequest;
+import com.google.android.gms.wearable.Wearable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayOutputStream;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+import ch.threema.app.ThreemaApplication;
+import ch.threema.app.utils.NameUtil;
+import ch.threema.app.utils.RuntimeUtil;
+import ch.threema.app.voip.services.CallRejectService;
+import ch.threema.app.voip.services.VoipCallService;
+import ch.threema.app.voip.services.VoipStateService;
+import ch.threema.app.voip.util.VoipUtil;
+import ch.threema.client.voip.VoipCallAnswerData;
+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_CONTACT_IDENTITY;
+import static ch.threema.app.voip.services.VoipStateService.TYPE_ACTIVITY;
+import static ch.threema.app.voip.services.VoipStateService.TYPE_NOTIFICATION;
+
+public class WearableHandler {
+	private static final Logger logger = LoggerFactory.getLogger(VoipStateService.class);
+
+	private final Context appContext;
+	private final DataClient.OnDataChangedListener wearableListener;
+
+	public WearableHandler(Context context) {
+		this.appContext = context;
+		this.wearableListener = new DataClient.OnDataChangedListener() {
+			@Override
+			public void onDataChanged(@NonNull DataEventBuffer eventsBuffer) {
+				final List<DataEvent> events = FreezableUtils.freezeIterable(eventsBuffer);
+				for (DataEvent event : events) {
+					if (event.getType() == DataEvent.TYPE_CHANGED) {
+						String path = event.getDataItem().getUri().getPath();
+						logger.info("onDataChanged Listener data event path {}", path);
+						if ("/accept-call".equals(path)) {
+							DataMapItem dataMapItem = DataMapItem.fromDataItem(event.getDataItem());
+							long callId = dataMapItem.getDataMap().getLong(EXTRA_CALL_ID, 0L);
+							String identity = dataMapItem.getDataMap().getString(EXTRA_CONTACT_IDENTITY);
+							final Intent intent = VoipStateService.createAcceptIntent(callId, identity);
+							appContext.startService(intent);
+							//Listen again for hang up
+							Wearable.getDataClient(appContext).addListener(wearableListener);
+
+						} if("/reject-call".equals(path)) {
+							DataMapItem dataMapItem = DataMapItem.fromDataItem(event.getDataItem());
+							long callId = dataMapItem.getDataMap().getLong(EXTRA_CALL_ID, 0L);
+							String identity = dataMapItem.getDataMap().getString(EXTRA_CONTACT_IDENTITY);
+							final Intent rejectIntent = VoipStateService.createRejectIntent(
+								callId,
+								identity,
+								VoipCallAnswerData.RejectReason.REJECTED
+							);
+							CallRejectService.enqueueWork(appContext, rejectIntent);
+						} if ("/disconnect-call".equals(path)){
+							VoipUtil.sendVoipCommand(appContext, VoipCallService.class, VoipCallService.ACTION_HANGUP);
+						}
+					}
+				}
+			}
+		};
+	}
+
+	/*
+	 *  Cancel notification or activity on wearable
+	 */
+	public static void cancelOnWearable(@VoipStateService.Component int component){
+		RuntimeUtil.runInAsyncTask(() -> {
+			try {
+				final List<Node> nodes = Tasks.await(
+					Wearable.getNodeClient(ThreemaApplication.getAppContext()).getConnectedNodes()
+				);
+				if (nodes != null) {
+					for (Node node : nodes) {
+						if (node.getId() != null) {
+							switch (component) {
+								case TYPE_NOTIFICATION:
+									Wearable.getMessageClient(ThreemaApplication.getAppContext())
+										.sendMessage(node.getId(), "/cancel-notification", null);
+									break;
+								case TYPE_ACTIVITY:
+									Wearable.getMessageClient(ThreemaApplication.getAppContext())
+										.sendMessage(node.getId(), "/cancel-activity", null);
+									break;
+								default:
+									break;
+							}
+						}
+					}
+				}
+			} catch (ExecutionException e) {
+				final String message = e.getMessage();
+				if (message != null && message.contains("Wearable.API is not available on this device")) {
+					logger.info("cancelOnWearable: ExecutionException while trying to connect to wearable: {}", message);
+				} else {
+					logger.info("cancelOnWearable: ExecutionException while trying to connect to wearable: {}", message);
+				}
+			} catch (InterruptedException e) {
+				logger.info("cancelOnWearable: Interrupted while waiting for wearable client");
+				// Restore interrupted state...
+				Thread.currentThread().interrupt();
+			}
+		});
+	}
+
+	/*
+	 *  Send information to the companion app on the wearable device
+	 */
+	@WorkerThread
+	public void showWearableNotification(
+		@NonNull ContactModel contact,
+		long callId,
+		@Nullable Bitmap avatar) {
+		final DataClient dataClient = Wearable.getDataClient(appContext);
+
+		// Add data to the request
+		final PutDataMapRequest putDataMapRequest = PutDataMapRequest.create("/incoming-call");
+		putDataMapRequest.getDataMap().putLong(EXTRA_CALL_ID, callId);
+		putDataMapRequest.getDataMap().putString(EXTRA_CONTACT_IDENTITY, contact.getIdentity());
+		logger.debug("sending the following contactIdentity from VoipState to wearable " + contact.getIdentity());
+		putDataMapRequest.getDataMap().putString("CONTACT_NAME", NameUtil.getDisplayNameOrNickname(contact, true));
+		putDataMapRequest.getDataMap().putLong("CALL_TIME", System.currentTimeMillis());
+		if (avatar != null) {
+			final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+			avatar.compress(Bitmap.CompressFormat.PNG, 100, buffer);
+			putDataMapRequest.getDataMap().putByteArray("CONTACT_AVATAR", buffer.toByteArray());
+		}
+
+		final PutDataRequest request = putDataMapRequest.asPutDataRequest();
+		request.setUrgent();
+
+		dataClient.addListener(this.wearableListener);
+		dataClient.putDataItem(request);
+	}
+}

+ 23 - 0
app/src/hms/AndroidManifest.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+          android:installLocation="internalOnly"
+          android:testOnly="false">
+
+	<application tools:ignore="GoogleAppIndexingWarning">
+
+		<activity
+			android:name="com.DrmSDK.DrmDialogActivity"
+			android:theme="@style/Theme.Threema.WithToolbar"/>
+
+		<service
+			android:name="ch.threema.app.push.PushService"
+			android:exported="false">
+			<intent-filter>
+				<action android:name="com.huawei.push.action.MESSAGING_EVENT" />
+			</intent-filter>
+		</service>
+
+	</application>
+
+</manifest>

+ 39 - 0
app/src/hms/agconnect-services.json

@@ -0,0 +1,39 @@
+{
+	"agcgw":{
+		"backurl":"connect-dre.dbankcloud.cn",
+		"url":"connect-dre.hispace.hicloud.com",
+		"websocketbackurl":"connect-ws-dre.hispace.dbankcloud.cn",
+		"websocketurl":"connect-ws-dre.hispace.dbankcloud.com"
+	},
+	"agcgw_all":{
+		"CN":"connect-drcn.hispace.hicloud.com",
+		"CN_back":"connect-drcn.dbankcloud.cn",
+		"DE":"connect-dre.hispace.hicloud.com",
+		"DE_back":"connect-dre.dbankcloud.cn",
+		"RU":"connect-drru.hispace.hicloud.com",
+		"RU_back":"connect-drru.dbankcloud.cn",
+		"SG":"connect-dra.hispace.hicloud.com",
+		"SG_back":"connect-dra.dbankcloud.cn"
+	},
+	"client":{
+		"cp_id":"5190041000024384032",
+		"product_id":"736430079244787738",
+		"client_id":"543649526116779072",
+		"project_id":"736430079244787738",
+		"app_id":"103713829",
+		"api_key":"CgB6e3x98OfTmUe8UCBVyRYd0YNHT43DjNTgXXxNV3MEWkr8+vKRC5vhyWbdX/JFZqDA+MTdmBPjCrx6YQWHm6aC",
+		"package_name":"ch.threema.app.hms"
+	},
+	"region":"DE",
+	"configuration_version":"2.0",
+	"appInfos":[
+		{
+			"package_name":"ch.threema.app.hms",
+			"app_id":"103713829"
+		},
+		{
+			"package_name":"ch.threema.app.work.hms",
+			"app_id":"103858571"
+		}
+	]
+}

+ 29 - 0
app/src/hms/java/ch/threema/app/activities/DownloadApkActivity.java

@@ -0,0 +1,29 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2019-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;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+public class DownloadApkActivity extends AppCompatActivity {
+	public static final String EXTRA_FORCE_UPDATE_DIALOG = "";
+	// stub
+}

+ 26 - 0
app/src/hms/java/ch/threema/app/utils/DownloadUtil.java

@@ -0,0 +1,26 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2020-2021 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.app.utils;
+
+public class DownloadUtil  {
+	//stub
+}

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

@@ -0,0 +1,105 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * 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.push;
+
+import android.content.Context;
+
+import com.huawei.agconnect.config.AGConnectServicesConfig;
+import com.huawei.hms.aaid.HmsInstanceId;
+import com.huawei.hms.common.ApiException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import androidx.annotation.NonNull;
+import androidx.work.Data;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+import ch.threema.app.utils.PushUtil;
+import ch.threema.base.ThreemaException;
+import ch.threema.client.ProtocolDefines;
+
+public class PushRegistrationWorker extends Worker {
+	private final Logger logger = LoggerFactory.getLogger(PushRegistrationWorker.class);
+
+	public static String TOKEN_SCOPE = "HCM";
+	public static String APP_ID_CONFIG_FIELD = "client/app_id";
+
+	private final Context appContext;
+
+	/**
+	 * Constructor for the PushRegistrationWorker.
+	 *
+	 * Note: This constructor is called by the WorkManager, so don't add additional parameters!
+	 */
+	public PushRegistrationWorker(@NonNull Context appContext, @NonNull WorkerParameters workerParams) {
+		super(appContext, workerParams);
+		this.appContext = appContext;
+	}
+
+	@NonNull
+	@Override
+	public Result doWork() {
+		Data workerFlags = getInputData();
+		final boolean clearToken = workerFlags.getBoolean(PushUtil.EXTRA_CLEAR_TOKEN, false);
+		final boolean withCallback = workerFlags.getBoolean(PushUtil.EXTRA_WITH_CALLBACK, false);
+		logger.debug("doWork HMS token registration clear {} withCallback {}", clearToken, withCallback);
+
+		if (clearToken) {
+			String error = null;
+			try {
+				// Obtain the app ID from the agconnect-service.json file.
+				String appId = AGConnectServicesConfig.fromContext(appContext).getString(APP_ID_CONFIG_FIELD);
+
+				// Delete the token.
+				HmsInstanceId.getInstance(appContext).deleteToken(appId, TOKEN_SCOPE);
+				PushUtil.sendTokenToServer(appContext,"", ProtocolDefines.PUSHTOKEN_TYPE_NONE);
+				logger.info("HMS token successfully deleted");
+			} catch (ApiException | ThreemaException e) {
+				logger.error("Exception", e);
+				error = e.getMessage();
+			}
+
+			if (withCallback) {
+				PushUtil.signalRegistrationFinished(error, clearToken);
+			}
+		}
+        else {
+			String appId = AGConnectServicesConfig.fromContext(appContext).getString(APP_ID_CONFIG_FIELD);
+			String error = null;
+			try {
+				String token = HmsInstanceId.getInstance(appContext).getToken(appId, TOKEN_SCOPE);
+				logger.info("Received HMS registration token");
+				PushUtil.sendTokenToServer(appContext, appId + '|' +token, ProtocolDefines.PUSHTOKEN_TYPE_HMS);
+			} catch (ThreemaException | ApiException e) {
+				logger.error("Exception", e);
+				error = e.getMessage();
+			}
+			if (withCallback) {
+				PushUtil.signalRegistrationFinished(error, clearToken);
+			}
+		}
+		// required by the Worker interface but is not used for any error handling in the push registration process
+		return Result.success();
+	}
+
+}

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

@@ -0,0 +1,118 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2015-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.push;
+
+import android.content.Context;
+import android.text.format.DateUtils;
+
+import com.huawei.agconnect.config.AGConnectServicesConfig;
+import com.huawei.hms.aaid.HmsInstanceId;
+import com.huawei.hms.api.ConnectionResult;
+import com.huawei.hms.api.HuaweiMobileServicesUtil;
+import com.huawei.hms.common.ApiException;
+import com.huawei.hms.push.HmsMessageService;
+import com.huawei.hms.push.RemoteMessage;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Date;
+import java.util.Map;
+
+import androidx.annotation.NonNull;
+import ch.threema.app.ThreemaApplication;
+import ch.threema.app.utils.PushUtil;
+import ch.threema.app.utils.RuntimeUtil;
+import ch.threema.base.ThreemaException;
+import ch.threema.client.ProtocolDefines;
+
+import static ch.threema.app.push.PushRegistrationWorker.APP_ID_CONFIG_FIELD;
+import static ch.threema.app.push.PushRegistrationWorker.TOKEN_SCOPE;
+
+public class PushService extends HmsMessageService {
+	private static final Logger logger = LoggerFactory.getLogger(PushService.class);
+
+	@Override
+	public void onNewToken(@NonNull String token) {
+		logger.info("New HMS token received");
+		try {
+			PushUtil.sendTokenToServer(this, token, ProtocolDefines.PUSHTOKEN_TYPE_HMS);
+		} catch (ThreemaException e) {
+			logger.error("onNewToken, could not send token to server ", e);
+		}
+	}
+
+	public static void deleteToken(Context context) {
+		String appId = AGConnectServicesConfig.fromContext(context).getString(APP_ID_CONFIG_FIELD);
+		try {
+			HmsInstanceId.getInstance(ThreemaApplication.getAppContext()).deleteToken(appId, TOKEN_SCOPE);
+			PushUtil.sendTokenToServer(context,"", ProtocolDefines.PUSHTOKEN_TYPE_NONE);
+		} catch (ApiException | ThreemaException e) {
+			logger.error("Could not delete hms token", e);
+		}
+	}
+
+	@Override
+	public void onMessageReceived(RemoteMessage remoteMessage) {
+		logger.info("Handling incoming HMS message.");
+
+		RuntimeUtil.runInWakelock(getApplicationContext(), DateUtils.MINUTE_IN_MILLIS * 10, "PushService", () -> processHMSMessage(remoteMessage));
+	}
+
+	private void processHMSMessage(RemoteMessage remoteMessage) {
+		logger.info("Received HMS message: {}", remoteMessage.getMessageId());
+		// Log message sent time
+		try {
+			Date sentDate = new Date(remoteMessage.getSentTime());
+			logger.info("*** Message sent     : " + sentDate.toString(), true);
+			logger.info("*** Message received : " + new Date().toString(), true);
+			logger.info("*** Original priority: " + remoteMessage.getOriginalUrgency());
+			logger.info("*** Current priority: " + remoteMessage.getUrgency());
+		} catch (Exception ignore) {
+		}
+
+		Map<String, String> data = remoteMessage.getDataOfMap();
+		PushUtil.processRemoteMessage(data);
+	}
+
+	// following services check are handled here and not in ConfigUtils to minimize number of duplicating classes
+	/**
+	 * check for specific huawei services
+	 */
+	public static boolean hmsServicesInstalled(Context context) {
+		return RuntimeUtil.isInTest() || (HuaweiMobileServicesUtil.isHuaweiMobileServicesAvailable(context) == ConnectionResult.SUCCESS);
+	}
+
+	/**
+	 * check for specific google services
+	 */
+	public static boolean playServicesInstalled(Context context) {
+		return false;
+	}
+
+	/**
+	 * check for available push service
+	 */
+	public static boolean servicesInstalled(Context context) {
+		return playServicesInstalled(context) || hmsServicesInstalled(context);
+	}
+}

+ 40 - 0
app/src/hms_services_based/java/ch/threema/app/wearable/WearableHandler.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.wearable;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+
+import ch.threema.app.voip.services.VoipStateService;
+import ch.threema.storage.models.ContactModel;
+
+/**
+ * stub for huawei builds because we have no Play Services on Huawei Builds and thus cannot communicate with a wearable
+ */
+public class WearableHandler {
+
+	public WearableHandler(Context context) {}
+
+	public static void cancelOnWearable(@VoipStateService.Component int component) {}
+
+	public void showWearableNotification(ContactModel contact, long callId, Bitmap avatar) {}
+}

+ 23 - 0
app/src/hms_work/AndroidManifest.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+          android:installLocation="internalOnly"
+          android:testOnly="false">
+
+	<application tools:ignore="GoogleAppIndexingWarning">
+
+		<activity
+			android:name="com.DrmSDK.DrmDialogActivity"
+			android:theme="@style/Theme.Threema.WithToolbar"/>
+
+		<service
+			android:name="ch.threema.app.push.PushService"
+			android:exported="false">
+			<intent-filter>
+				<action android:name="com.huawei.push.action.MESSAGING_EVENT" />
+			</intent-filter>
+		</service>
+
+	</application>
+
+</manifest>

+ 39 - 0
app/src/hms_work/agconnect-services.json

@@ -0,0 +1,39 @@
+{
+	"agcgw":{
+		"backurl":"connect-dre.dbankcloud.cn",
+		"url":"connect-dre.hispace.hicloud.com",
+		"websocketbackurl":"connect-ws-dre.hispace.dbankcloud.cn",
+		"websocketurl":"connect-ws-dre.hispace.dbankcloud.com"
+	},
+	"agcgw_all":{
+		"CN":"connect-drcn.hispace.hicloud.com",
+		"CN_back":"connect-drcn.dbankcloud.cn",
+		"DE":"connect-dre.hispace.hicloud.com",
+		"DE_back":"connect-dre.dbankcloud.cn",
+		"RU":"connect-drru.hispace.hicloud.com",
+		"RU_back":"connect-drru.dbankcloud.cn",
+		"SG":"connect-dra.hispace.hicloud.com",
+		"SG_back":"connect-dra.dbankcloud.cn"
+	},
+	"client":{
+		"cp_id":"5190041000024384032",
+		"product_id":"736430079244787738",
+		"client_id":"543649526116779072",
+		"project_id":"736430079244787738",
+		"app_id":"103858571",
+		"api_key":"CgB6e3x98OfTmUe8UCBVyRYd0YNHT43DjNTgXXxNV3MEWkr8+vKRC5vhyWbdX/JFZqDA+MTdmBPjCrx6YQWHm6aC",
+		"package_name":"ch.threema.app.work.hms"
+	},
+	"region":"DE",
+	"configuration_version":"2.0",
+	"appInfos":[
+		{
+			"package_name":"ch.threema.app.hms",
+			"app_id":"103713829"
+		},
+		{
+			"package_name":"ch.threema.app.work.hms",
+			"app_id":"103858571"
+		}
+	]
+}

+ 29 - 0
app/src/hms_work/java/ch/threema/app/activities/DownloadApkActivity.java

@@ -0,0 +1,29 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2019-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;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+public class DownloadApkActivity extends AppCompatActivity {
+	public static final String EXTRA_FORCE_UPDATE_DIALOG = "";
+	// stub
+}

+ 26 - 0
app/src/hms_work/java/ch/threema/app/utils/DownloadUtil.java

@@ -0,0 +1,26 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2020-2021 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.app.utils;
+
+public class DownloadUtil  {
+	//stub
+}

+ 1 - 41
app/src/main/AndroidManifest.xml

@@ -46,7 +46,7 @@
 
 	<!-- PROTECTION_NORMAL - granted automatically -->
 	<uses-permission android:name="android.permission.INTERNET"/>
-	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
 	<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
 	<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
 	<uses-permission android:name="android.permission.VIBRATE"/>
@@ -133,7 +133,6 @@
 		android:theme="@style/AppBaseTheme"
 		android:usesCleartextTraffic="false"
 		android:networkSecurityConfig="@xml/network_security_config"
-		android:requestLegacyExternalStorage="true"
 		android:manageSpaceActivity=".activities.StorageManagementActivity"
 		android:allowAudioPlaybackCapture="false"
 		android:appCategory="social"
@@ -144,27 +143,9 @@
 		<meta-data
 			android:name="android.max_aspect"
 			android:value="2.5"/>
-		<meta-data
-			android:name="firebase_analytics_collection_deactivated"
-			android:value="true"/>
-		<meta-data
-			android:name="google_analytics_adid_collection_enabled"
-			android:value="false"/>
-		<meta-data
-			android:name="google_analytics_ssaid_collection_enabled"
-			android:value="false" />
-		<meta-data
-			android:name="firebase_messaging_auto_init_enabled"
-			android:value="false"/>
 		<meta-data
 			android:name="android.provider.CONTACTS_STRUCTURE"
 			android:resource="@xml/contacts"/>
-		<meta-data
-			android:name="com.google.android.gms.version"
-			android:value="@integer/google_play_services_version"/>
-		<meta-data
-			android:name="com.google.android.gms.car.application"
-			android:resource="@xml/automotive_app_desc"/>
 
 		<activity
 			android:name=".activities.MainActivity"
@@ -416,11 +397,6 @@
 			android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
 			android:theme="@style/Theme.Threema.Translucent">
 		</activity>
-		<activity
-			android:name=".activities.NotesEditActivity"
-			android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
-			android:theme="@style/Theme.Threema.Translucent">
-		</activity>
 		<activity
 			android:name=".activities.GroupDetailActivity"
 			android:theme="@style/Theme.Threema.TransparentStatusbar"
@@ -810,17 +786,6 @@
 			android:permission="android.permission.BIND_JOB_SERVICE"
 			android:enabled="true"
 			android:exported="false"/>
-		<service
-			android:name=".FcmListenerService">
-			<intent-filter>
-				<action android:name="com.google.firebase.MESSAGING_EVENT" />
-			</intent-filter>
-		</service>
-		<service
-			android:name=".FcmRegistrationIntentService"
-			android:exported="false"
-			android:permission="android.permission.BIND_JOB_SERVICE">
-		</service>
 		<service
 			android:name=".RecipientChooserTargetService"
 			android:label="@string/app_name"
@@ -872,11 +837,6 @@
 			android:enabled="true"
 			android:exported="false"
 			android:permission="android.permission.BIND_JOB_SERVICE"/>
-		<service
-			android:name=".jobs.FcmRegistrationJobService"
-			android:enabled="true"
-			android:exported="false"
-			android:permission="android.permission.BIND_JOB_SERVICE"/>
 
 		<!-- broadcast receivers -->
 		<receiver

+ 11 - 1
app/src/main/java/ch/threema/app/BuildFlavor.java

@@ -29,9 +29,11 @@ public class BuildFlavor {
 	private final static String FLAVOR_SANDBOX = "sandbox";
 	private final static String FLAVOR_SANDBOX_WORK = "sandbox_work";
 	private final static String FLAVOR_RED = "red";
+	private final static String FLAVOR_HMS = "hms";
+	private final static String FLAVOR_HMS_WORK = "hms_work";
 
 	public enum LicenseType {
-		NONE, GOOGLE, SERIAL, GOOGLE_WORK
+		NONE, GOOGLE, SERIAL, GOOGLE_WORK, HMS, HMS_WORK
 	}
 
 	private static boolean initialized = false;
@@ -112,6 +114,14 @@ public class BuildFlavor {
 					name = "Red";
 					licenseType = LicenseType.GOOGLE_WORK;
 					break;
+				case FLAVOR_HMS:
+					name = "HMS";
+					licenseType = LicenseType.HMS;
+					break;
+				case FLAVOR_HMS_WORK:
+					name = "Hms Work";
+					licenseType = LicenseType.HMS_WORK;
+					break;
 				default:
 					throw new RuntimeException("invalid flavor build " + BuildConfig.FLAVOR);
 			}

+ 0 - 418
app/src/main/java/ch/threema/app/FcmListenerService.java

@@ -1,418 +0,0 @@
-/*  _____ _
- * |_   _| |_  _ _ ___ ___ _ __  __ _
- *   | | | ' \| '_/ -_) -_) '  \/ _` |_
- *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
- *
- * Threema for Android
- * Copyright (c) 2015-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.app.job.JobInfo;
-import android.app.job.JobScheduler;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.net.Uri;
-import android.os.Build;
-import android.text.format.DateUtils;
-
-import com.google.firebase.messaging.FirebaseMessagingService;
-import com.google.firebase.messaging.RemoteMessage;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.Date;
-import java.util.Map;
-
-import ch.threema.app.jobs.ReConnectJobService;
-import ch.threema.app.managers.ServiceManager;
-import ch.threema.app.receivers.AlarmManagerBroadcastReceiver;
-import ch.threema.app.services.DeadlineListService;
-import ch.threema.app.services.LockAppService;
-import ch.threema.app.services.NotificationService;
-import ch.threema.app.services.NotificationServiceImpl;
-import ch.threema.app.services.PollingHelper;
-import ch.threema.app.services.PreferenceService;
-import ch.threema.app.services.PreferenceServiceImpl;
-import ch.threema.app.services.RingtoneService;
-import ch.threema.app.stores.PreferenceStore;
-import ch.threema.app.utils.PushUtil;
-import ch.threema.app.utils.RuntimeUtil;
-import ch.threema.app.webclient.services.SessionWakeUpServiceImpl;
-
-public class FcmListenerService extends FirebaseMessagingService {
-	private static final Logger logger = LoggerFactory.getLogger(FcmListenerService.class);
-
-	private static final int RECONNECT_JOB = 89;
-
-	private static final String WEBCLIENT_SESSION = "wcs";
-	private static final String WEBCLIENT_TIMESTAMP = "wct";
-	private static final String WEBCLIENT_VERSION = "wcv";
-	private static final String WEBCLIENT_AFFILIATION_ID = "wca";
-
-	private static final int NOTIFICATION_TYPE_LOCKED = 3;
-
-	private PollingHelper pollingHelper = null;
-
-	@Override
-	public void onNewToken(String token) {
-		logger.info("New FCM token received");
-
-		// Fetch updated Instance ID token and notify our app's server of any changes (if applicable).
-		PushUtil.clearPushTokenSentDate(this);
-		PushUtil.scheduleSendPushTokenToServer(this);
-	}
-
-	@Override
-	public void onMessageReceived(RemoteMessage remoteMessage) {
-		logger.info("Handling incoming FCM intent.");
-
-		RuntimeUtil.runInWakelock(getApplicationContext(), DateUtils.MINUTE_IN_MILLIS * 10, "FcmListenerService", () -> processFcmMessage(remoteMessage));
-	}
-
-	private void processFcmMessage(RemoteMessage remoteMessage) {
-		logger.info("Received FCM message: {}", remoteMessage.getMessageId());
-
-		// from should be equal to R.string.gcm_sender_id
-		String from = remoteMessage.getFrom();
-		Map<String, String> data = remoteMessage.getData();
-
-		if (pollingHelper == null) {
-			pollingHelper = new PollingHelper(this, "FCM");
-		}
-
-		// Log message sent time
-		try {
-			Date sentDate = new Date(remoteMessage.getSentTime());
-
-			logger.info("*** Message sent     : " + sentDate.toString(), true);
-			logger.info("*** Message received : " + new Date().toString(), true);
-			logger.info("*** Original priority: " + remoteMessage.getOriginalPriority());
-			logger.info("*** Current priority: " + remoteMessage.getPriority());
-		} catch (Exception ignore) {
-		}
-
-		// Message notifications
-		if (remoteMessage.getCollapseKey() != null && remoteMessage.getCollapseKey().equals("new_message")) {
-			// Post notification of received message.
-			sendNotification();
-		}
-
-		// Webclient notifications
-		if (data != null && data.containsKey(WEBCLIENT_SESSION) && data.containsKey(WEBCLIENT_TIMESTAMP)) {
-			final String session = data.get(WEBCLIENT_SESSION);
-			final String timestamp = data.get(WEBCLIENT_TIMESTAMP);
-			final String version = data.get(WEBCLIENT_VERSION);
-			final String affiliationId = data.get(WEBCLIENT_AFFILIATION_ID);
-			if (session != null && !session.isEmpty() && timestamp != null && !timestamp.isEmpty()) {
-				logger.debug("Received FCM webclient wakeup for session {}", session);
-
-				final Thread t = new Thread(() -> {
-					logger.info("Trying to wake up webclient session {}", session);
-
-					// Parse version number
-					Integer versionNumber = null;
-					if (version != null) { // Can be null during beta, if an old client doesn't yet send the version field
-						try {
-							versionNumber = Integer.parseInt(version);
-						} catch (NumberFormatException e) {
-							// Version number was sent but is not a valid u16.
-							// We should probably throw the entire wakeup notification away.
-							logger.error("Could not parse webclient protocol version number: " + e);
-							return;
-						}
-					}
-
-					// Try to wake up session
-					SessionWakeUpServiceImpl.getInstance()
-							.resume(session, versionNumber == null ? 0 : versionNumber, affiliationId);
-				});
-				t.setName("webclient-gcm-wakeup");
-				t.start();
-			}
-		}
-	}
-
-	private void displayAdHocNotification(int type) {
-		final ServiceManager serviceManager = ThreemaApplication.getServiceManager();
-
-		NotificationService notificationService;
-		if (serviceManager != null) {
-			notificationService = serviceManager.getNotificationService();
-		} else {
-			// Because the master key is locked, there is no preference service object.
-			// We need to create one for ourselves so that we can read the user's notification prefs
-			// (which are unencrypted).
-
-			//create a temporary service class (with some implementations) to use the showMasterKeyLockedNewMessageNotification
-
-			PreferenceStore ps = new PreferenceStore(this.getApplicationContext(), ThreemaApplication.getMasterKey());
-			PreferenceService p = new PreferenceServiceImpl(this.getApplicationContext(), ps);
-
-			Context c = this.getApplicationContext();
-
-			notificationService = new NotificationServiceImpl(
-				c,
-				new LockAppService() {
-					@Override
-					public boolean isLockingEnabled() {
-						return false;
-					}
-
-					@Override
-					public boolean unlock(String pin) {
-						return false;
-					}
-
-					@Override
-					public void lock() {
-					}
-
-					@Override
-					public boolean checkLock() {
-						return false;
-					}
-
-					@Override
-					public boolean isLocked() {
-						return false;
-					}
-
-					@Override
-					public LockAppService resetLockTimer(boolean restartAfterReset) {
-						return null;
-					}
-
-					@Override
-					public void addOnLockAppStateChanged(OnLockAppStateChanged c) {
-					}
-
-					@Override
-					public void removeOnLockAppStateChanged(OnLockAppStateChanged c) {
-					}
-				},
-				new DeadlineListService() {
-					@Override
-					public void add(String uid, long timeout) {
-					}
-
-					@Override
-					public void init() {
-					}
-
-					@Override
-					public boolean has(String uid) {
-						return false;
-					}
-
-					@Override
-					public void remove(String uid) {
-					}
-
-					@Override
-					public long getDeadline(String uid) {
-						return 0;
-					}
-
-					@Override
-					public int getSize() {
-						return 0;
-					}
-
-					@Override
-					public void clear() {
-					}
-				},
-				new DeadlineListService() {
-					@Override
-					public void add(String uid, long timeout) {
-					}
-
-					@Override
-					public void init() {
-					}
-
-					@Override
-					public boolean has(String uid) {
-						return false;
-					}
-
-					@Override
-					public void remove(String uid) {
-					}
-
-					@Override
-					public long getDeadline(String uid) {
-						return 0;
-					}
-
-					@Override
-					public int getSize() {
-						return 0;
-					}
-
-					@Override
-					public void clear() {
-					}
-				},
-				new DeadlineListService() {
-					@Override
-					public void add(String uid, long timeout) {}
-
-					@Override
-					public void init() {}
-
-					@Override
-					public boolean has(String uid) { return false; }
-
-					@Override
-					public void remove(String uid) {}
-
-					@Override
-					public long getDeadline(String uid) { return 0; }
-
-					@Override
-					public int getSize() { return 0; }
-
-					@Override
-					public void clear() {}
-				},
-				p,
-				new RingtoneService() {
-					@Override
-					public void init() {
-					}
-
-					@Override
-					public void setRingtone(String uniqueId, Uri ringtoneUri) {
-					}
-
-					@Override
-					public Uri getRingtoneFromUniqueId(String uniqueId) {
-						return null;
-					}
-
-					@Override
-					public boolean hasCustomRingtone(String uniqueId) {
-						return false;
-					}
-
-					@Override
-					public void removeCustomRingtone(String uniqueId) {
-					}
-
-					@Override
-					public void resetRingtones(Context context) {
-					}
-
-					@Override
-					public Uri getContactRingtone(String uniqueId) {
-						return null;
-					}
-
-					@Override
-					public Uri getGroupRingtone(String uniqueId) {
-						return null;
-					}
-
-					@Override
-					public Uri getVoiceCallRingtone(String uniqueId) {
-						return null;
-					}
-
-					@Override
-					public Uri getDefaultContactRingtone() {
-						return null;
-					}
-
-					@Override
-					public Uri getDefaultGroupRingtone() {
-						return null;
-					}
-
-					@Override
-					public boolean isSilent(String uniqueId, boolean isGroup) {
-						return false;
-					}
-				});
-		}
-
-		if (notificationService != null) {
-			notificationService.showMasterKeyLockedNewMessageNotification();
-		}
-	}
-
-	@Override
-	public void onTaskRemoved(Intent rootIntent) {
-		logger.debug("*** Service task removed");
-		super.onTaskRemoved(rootIntent);
-	}
-
-	private void sendNotification() {
-		// check if background data is disabled before attempting to connect
-		ConnectivityManager mgr = (ConnectivityManager) this.getSystemService(Context.CONNECTIVITY_SERVICE);
-		NetworkInfo networkInfo = mgr.getActiveNetworkInfo();
-		if (networkInfo != null && networkInfo.getDetailedState() == NetworkInfo.DetailedState.BLOCKED) {
-			logger.warn("Network blocked (background data disabled?)");
-			// The same GCM message may arrive twice (due to a network change). so we simply ignore messages that we were unable to fetch due to a blocked network
-			// Simply schedule a poll when the device is back online
-			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-				JobScheduler js = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
-
-				js.cancel(RECONNECT_JOB);
-
-				JobInfo job = new JobInfo.Builder(RECONNECT_JOB,
-						new ComponentName(this, ReConnectJobService.class))
-						.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
-						.setRequiresCharging(false)
-						.build();
-
-				if (js.schedule(job) != JobScheduler.RESULT_SUCCESS) {
-					logger.error("Job scheduling failed");
-				}
-			}
-			return;
-		}
-
-		if (networkInfo == null) {
-			logger.warn("No network info available");
-		}
-
-		//recheck after one minute
-		AlarmManagerBroadcastReceiver.requireLoggedInConnection(
-				this,
-				(int) DateUtils.MINUTE_IN_MILLIS
-		);
-
-		PreferenceStore preferenceStore = new PreferenceStore(this, null);
-		PreferenceServiceImpl preferenceService = new PreferenceServiceImpl(this, preferenceStore);
-
-		if (ThreemaApplication.getMasterKey() != null &&
-			ThreemaApplication.getMasterKey().isLocked() &&
-			preferenceService.isMasterKeyNewMessageNotifications()) {
-
-			displayAdHocNotification(NOTIFICATION_TYPE_LOCKED);
-		}
-
-		if (!pollingHelper.poll(true)) {
-			logger.warn("Unable to establish connection");
-		}
-	}
-}

+ 0 - 148
app/src/main/java/ch/threema/app/FcmRegistrationIntentService.java

@@ -1,148 +0,0 @@
-/*  _____ _
- * |_   _| |_  _ _ ___ ___ _ __  __ _
- *   | | | ' \| '_/ -_) -_) '  \/ _` |_
- *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
- *
- * Threema for Android
- * Copyright (c) 2019-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.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.text.TextUtils;
-
-import com.google.android.gms.tasks.OnCompleteListener;
-import com.google.android.gms.tasks.OnFailureListener;
-import com.google.android.gms.tasks.Task;
-import com.google.firebase.iid.FirebaseInstanceId;
-import com.google.firebase.iid.InstanceIdResult;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-
-import androidx.annotation.NonNull;
-import androidx.core.app.FixedJobIntentService;
-import androidx.localbroadcastmanager.content.LocalBroadcastManager;
-import androidx.preference.PreferenceManager;
-import ch.threema.app.managers.ServiceManager;
-import ch.threema.base.ThreemaException;
-import ch.threema.client.ProtocolDefines;
-import ch.threema.client.ThreemaConnection;
-
-public class FcmRegistrationIntentService extends FixedJobIntentService {
-	private static final Logger logger = LoggerFactory.getLogger(FcmRegistrationIntentService.class);
-
-	public static final String EXTRA_CLEAR_TOKEN = "clear";
-	public static final String EXTRA_WITH_CALLBACK = "cb";
-
-	private static final int JOB_ID = 2002;
-
-	public static void enqueueWork(Context context, Intent work) {
-		enqueueWork(context, FcmRegistrationIntentService.class, JOB_ID, work);
-	}
-
-	@Override
-	protected void onHandleWork(@NonNull Intent intent) {
-		final boolean clearToken = intent.hasExtra(EXTRA_CLEAR_TOKEN);
-		final boolean withCallback = intent.hasExtra(EXTRA_WITH_CALLBACK);
-		final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
-
-		if (clearToken) {
-			String error = null;
-			try {
-				FirebaseInstanceId.getInstance().deleteInstanceId();
-			} catch (IOException e) {
-				error = "could not delete firebase instance id";
-			}
-			sendRegistrationToServer("", sharedPreferences);
-			signalRegistrationFinished(error, withCallback, clearToken, sharedPreferences);
-		} else {
-			try {
-				FirebaseInstanceId.getInstance().getInstanceId()
-						.addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
-							@Override
-							public void onComplete(@NonNull Task<InstanceIdResult> task) {
-								String error = null;
-								if (task.isSuccessful() && task.getResult() != null && !TextUtils.isEmpty(task.getResult().getToken())) {
-									String token = task.getResult().getToken();
-									logger.debug(String.format("Got FCM Registration Token: %s", token));
-									sendRegistrationToServer(token, sharedPreferences);
-								} else {
-									error = task.getException().getMessage();
-								}
-								signalRegistrationFinished(error, withCallback, clearToken, sharedPreferences);
-							}
-						})
-						.addOnFailureListener(new OnFailureListener() {
-							@Override
-							public void onFailure(@NonNull Exception e) {
-								signalRegistrationFinished(e.getMessage(), withCallback, clearToken, sharedPreferences);
-							}
-						});
-			} catch (IllegalStateException e) {
-				signalRegistrationFinished(e.getMessage(), withCallback, clearToken, sharedPreferences);
-			}
-		}
-	}
-
-	private void signalRegistrationFinished(String error, boolean withCallback, boolean clearToken, SharedPreferences sharedPreferences) {
-		if (error != null) {
-			logger.warn(String.format("Failed to get FCM token from Firebase: %s", error));
-			sharedPreferences.edit().putLong(getString(R.string.preferences__gcm_token_sent_date), 0L).apply();
-		}
-
-		if (withCallback) {
-			// Notify UI that registration has completed, so the progress indicator can be hidden.
-
-			final Intent intent = new Intent(ThreemaApplication.INTENT_GCM_REGISTRATION_COMPLETE);
-			intent.putExtra(EXTRA_CLEAR_TOKEN, clearToken);
-			LocalBroadcastManager.getInstance(FcmRegistrationIntentService.this).sendBroadcast(intent);
-		}
-	}
-
-	private boolean sendRegistrationToServer(String token, SharedPreferences sharedPreferences) {
-		ServiceManager serviceManager = ThreemaApplication.getServiceManager();
-
-		if (serviceManager != null) {
-			ThreemaConnection connection = serviceManager.getConnection();
-
-			if (connection != null) {
-				try {
-					connection.setPushToken(ProtocolDefines.PUSHTOKEN_TYPE_GCM, token);
-					logger.info("FCM token successfully sent to server");
-
-					// Save current token
-					sharedPreferences.edit().putLong(getString(R.string.preferences__gcm_token_sent_date), System.currentTimeMillis()).apply();
-					// Used in the Webclient Sessions
-					serviceManager.getPreferenceService().setPushToken(token);
-					return true;
-				} catch (ThreemaException e) {
-					logger.warn("Unable to send token to server: " + e.getMessage());
-				}
-			} else {
-				logger.warn("FCM token send failed: no connection");
-			}
-		} else {
-			logger.error("FCM token send failed: no servicemanager");
-		}
-		sharedPreferences.edit().putLong(getString(R.string.preferences__gcm_token_sent_date), 0L).apply();
-		return false;
-	}
-}

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

@@ -110,6 +110,7 @@ import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.messagereceiver.ContactMessageReceiver;
 import ch.threema.app.messagereceiver.GroupMessageReceiver;
 import ch.threema.app.messagereceiver.MessageReceiver;
+import ch.threema.app.push.PushService;
 import ch.threema.app.receivers.ConnectivityChangeReceiver;
 import ch.threema.app.receivers.PinningFailureReportBroadcastReceiver;
 import ch.threema.app.receivers.RestrictBackgroundChangedReceiver;
@@ -214,7 +215,7 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 	public static final String INTENT_DATA_CHECK_ONLY = "check";
 	public static final String INTENT_DATA_ANIM_CENTER = "itemPos";
 	public static final String INTENT_DATA_PICK_FROM_CAMERA = "useCam";
-	public static final String INTENT_GCM_REGISTRATION_COMPLETE = "registrationComplete";
+	public static final String INTENT_PUSH_REGISTRATION_COMPLETE = "registrationComplete";
 	public static final String INTENT_DATA_PIN = "ppin";
 	public static final String INTENT_DATA_HIDE_RECENTS = "hiderec";
 	public static final String INTENT_ACTION_FORWARD = "ch.threema.app.intent.FORWARD";
@@ -873,12 +874,12 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 			connection.addConnectionStateListener((newConnectionState, address) -> {
 				if (newConnectionState == ConnectionState.LOGGEDIN) {
 					final Context appContext = getAppContext();
-					if (ConfigUtils.isPlayServicesInstalled(appContext)) {
+					if (PushService.servicesInstalled(appContext)) {
 						if (PushUtil.isPushEnabled(appContext)) {
 							if (PushUtil.pushTokenNeedsRefresh(appContext)) {
-								PushUtil.scheduleSendPushTokenToServer(appContext);
+								PushUtil.enqueuePushTokenUpdate(appContext, false, false);
 							} else {
-								logger.debug("FCM token is still fresh. No update needed");
+								logger.debug("Push token is still fresh. No update needed");
 							}
 						}
 					}

+ 17 - 10
app/src/main/java/ch/threema/app/activities/DirectoryActivity.java

@@ -69,6 +69,7 @@ import ch.threema.app.ui.ThreemaSearchView;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.IntentDataUtil;
 import ch.threema.app.utils.LogUtil;
+import ch.threema.app.utils.TestUtil;
 import ch.threema.client.work.WorkDirectoryCategory;
 import ch.threema.client.work.WorkDirectoryContact;
 import ch.threema.client.work.WorkOrganization;
@@ -171,7 +172,7 @@ public class DirectoryActivity extends ThreemaToolbarActivity implements Threema
 		}
 
 		WorkOrganization workOrganization = preferenceService.getWorkOrganization();
-		if (workOrganization != null) {
+		if (workOrganization != null && !TestUtil.empty(workOrganization.getName())) {
 			logger.info("Organization: " + workOrganization.getName());
 			getToolbar().setTitle(workOrganization.getName());
 		}
@@ -252,16 +253,22 @@ public class DirectoryActivity extends ThreemaToolbarActivity implements Threema
 	}
 
 	private void launchContact(final WorkDirectoryContact workDirectoryContact, final int position) {
-		if (contactService.getByIdentity(workDirectoryContact.threemaId) == null) {
-			addContact(workDirectoryContact, new Runnable() {
-				@Override
-				public void run() {
-					openContact(workDirectoryContact.threemaId);
-					directoryAdapter.notifyItemChanged(position);
-				}
-			});
+		if (workDirectoryContact.threemaId != null) {
+			if (contactService.getByIdentity(workDirectoryContact.threemaId) == null) {
+				addContact(workDirectoryContact, new Runnable() {
+					@Override
+					public void run() {
+						openContact(workDirectoryContact.threemaId);
+						directoryAdapter.notifyItemChanged(position);
+					}
+				});
+			} else if (workDirectoryContact.threemaId.equalsIgnoreCase(contactService.getMe().getIdentity())) {
+				Toast.makeText(this, R.string.me_myself_and_i, Toast.LENGTH_LONG).show();
+			} else {
+				openContact(workDirectoryContact.threemaId);
+			}
 		} else {
-			openContact(workDirectoryContact.threemaId);
+			Toast.makeText(this, R.string.contact_not_found, Toast.LENGTH_LONG).show();
 		}
 	}
 

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

@@ -48,6 +48,7 @@ import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.dialogs.GenericProgressDialog;
 import ch.threema.app.exceptions.FileSystemNotPresentException;
+import ch.threema.app.push.PushService;
 import ch.threema.app.services.AppRestrictionService;
 import ch.threema.app.services.license.LicenseService;
 import ch.threema.app.services.license.LicenseServiceUser;
@@ -72,6 +73,7 @@ public class EnterSerialActivity extends ThreemaActivity {
 	private Button loginButton;
 	private LicenseService licenseService;
 
+	@SuppressLint("StringFormatInvalid")
 	public void onCreate(Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
 
@@ -128,7 +130,12 @@ public class EnterSerialActivity extends ThreemaActivity {
 		} else {
 			privateExplainText = findViewById(R.id.private_explain);
 			if (privateExplainText != null) {
-				privateExplainText.setText(Html.fromHtml(getString(R.string.private_threema_download)));
+				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))));
+				}
 				privateExplainText.setClickable(true);
 				privateExplainText.setMovementMethod (LinkMovementMethod.getInstance());
 			}

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

@@ -59,6 +59,7 @@ import java.io.File;
 import java.lang.ref.WeakReference;
 import java.net.InetSocketAddress;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Date;
 import java.util.Iterator;
 import java.util.List;
@@ -76,6 +77,7 @@ import androidx.fragment.app.FragmentTransaction;
 import androidx.lifecycle.LifecycleOwner;
 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
 import ch.threema.app.BuildFlavor;
+import ch.threema.app.push.PushService;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.activities.wizard.WizardBaseActivity;
@@ -208,8 +210,9 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
 				@Override
 				public void run() {
 					if (intent.getAction().equals(IntentDataUtil.ACTION_LICENSE_NOT_ALLOWED)) {
-						if (BuildFlavor.getLicenseType() == BuildFlavor.LicenseType.SERIAL ||
-							BuildFlavor.getLicenseType() == BuildFlavor.LicenseType.GOOGLE_WORK) {
+						if (Arrays.asList(BuildFlavor.LicenseType.SERIAL,
+								BuildFlavor.LicenseType.GOOGLE_WORK,
+								BuildFlavor.LicenseType.HMS_WORK).contains(BuildFlavor.getLicenseType())) {
 							//show enter serial stuff
 							startActivityForResult(new Intent(HomeActivity.this, EnterSerialActivity.class), ThreemaActivity.ACTIVITY_ID_ENTER_SERIAL);
 						} else {
@@ -697,7 +700,7 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
 				CheckLicenseRoutine check = null;
 				try {
 					check = new CheckLicenseRoutine(
-							getApplicationContext(),
+							this,
 							serviceManager.getAPIConnector(),
 							serviceManager.getUserService(),
 							deviceService,
@@ -940,7 +943,7 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
 		this.invalidateOptionsMenu();
 
 		if (savedInstanceState == null) {
-			if (!ConfigUtils.isPlayServicesInstalled(this)) {
+			if (!PushService.servicesInstalled(this)) {
 				enablePolling(serviceManager);
 				if (!ConfigUtils.isBlackBerry() && !ConfigUtils.isAmazonDevice() && !ConfigUtils.isWorkBuild()) {
 					RuntimeUtil.runOnUiThread(() -> ShowOnceDialog.newInstance(R.string.push_not_available_title, R.string.push_not_available_text).show(getSupportFragmentManager(), "nopush"));

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

@@ -91,6 +91,8 @@ import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.DialogUtil;
 import ch.threema.app.utils.TestUtil;
 
+import static ch.threema.app.utils.BitmapUtil.FLIP_NONE;
+
 public class ImagePaintActivity extends ThreemaToolbarActivity implements GenericAlertDialog.DialogClickListener {
 	private static final Logger logger = LoggerFactory.getLogger(ImagePaintActivity.class);
 
@@ -454,9 +456,6 @@ public class ImagePaintActivity extends ThreemaToolbarActivity implements Generi
 					return null;
 				}
 
-				originalImageWidth = options.outWidth;
-				originalImageHeight = options.outHeight;
-
 				options.inPreferredConfig = Bitmap.Config.ARGB_8888;
 				options.inJustDecodeBounds = false;
 
@@ -464,8 +463,17 @@ public class ImagePaintActivity extends ThreemaToolbarActivity implements Generi
 					if (data != null) {
 						orgBitmap = BitmapFactory.decodeStream(new BufferedInputStream(data), null, options);
 						if (orgBitmap != null) {
+							if (exifOrientation != 0 || exifFlip != FLIP_NONE) {
+								orgBitmap = BitmapUtil.rotateBitmap(orgBitmap, exifOrientation, exifFlip);
+							}
+							if (orientation != 0 || flip != FLIP_NONE) {
+								orgBitmap = BitmapUtil.rotateBitmap(orgBitmap, orientation, flip);
+							}
 							bitmap = Bitmap.createBitmap(orgBitmap.getWidth() & ~0x1, orgBitmap.getHeight(), Bitmap.Config.RGB_565);
 							new Canvas(bitmap).drawBitmap(orgBitmap, 0, 0, null);
+
+							originalImageWidth = orgBitmap.getWidth();
+							originalImageHeight = orgBitmap.getHeight();
 						} else {
 							logger.info("could not open image");
 							return null;

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

@@ -28,6 +28,7 @@ import android.content.res.Configuration;
 import android.content.res.TypedArray;
 import android.os.AsyncTask;
 import android.os.Bundle;
+import android.os.Handler;
 import android.util.SparseBooleanArray;
 import android.view.ActionMode;
 import android.view.Menu;
@@ -36,8 +37,8 @@ import android.view.View;
 import android.widget.AbsListView;
 import android.widget.AdapterView;
 import android.widget.FrameLayout;
-import android.widget.GridView;
 import android.widget.ProgressBar;
+import android.widget.TextView;
 
 import com.google.android.material.snackbar.Snackbar;
 
@@ -47,6 +48,7 @@ import org.slf4j.LoggerFactory;
 import java.io.File;
 import java.sql.SQLException;
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -68,12 +70,14 @@ import ch.threema.app.services.FileService;
 import ch.threema.app.services.GroupService;
 import ch.threema.app.services.MessageService;
 import ch.threema.app.ui.EmptyView;
+import ch.threema.app.ui.FastScrollGridView;
 import ch.threema.app.utils.AnimationUtil;
 import ch.threema.app.utils.AppRestrictionUtil;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.DialogUtil;
 import ch.threema.app.utils.FileUtil;
 import ch.threema.app.utils.IntentDataUtil;
+import ch.threema.app.utils.LocaleUtil;
 import ch.threema.app.utils.LogUtil;
 import ch.threema.app.utils.NameUtil;
 import ch.threema.app.utils.RuntimeUtil;
@@ -85,7 +89,9 @@ import ch.threema.storage.models.GroupModel;
 import ch.threema.storage.models.MessageType;
 import ch.threema.storage.models.data.MessageContentsType;
 
-public class MediaGalleryActivity extends ThreemaToolbarActivity implements AdapterView.OnItemClickListener, ActionBar.OnNavigationListener, GenericAlertDialog.DialogClickListener {
+import static ch.threema.app.fragments.ComposeMessageFragment.SCROLLBUTTON_VIEW_TIMEOUT;
+
+public class MediaGalleryActivity extends ThreemaToolbarActivity implements AdapterView.OnItemClickListener, ActionBar.OnNavigationListener, GenericAlertDialog.DialogClickListener, FastScrollGridView.ScrollListener {
 	private static final Logger logger = LoggerFactory.getLogger(MediaGalleryActivity.class);
 
 	private ThumbnailCache<?> thumbnailCache = null;
@@ -96,12 +102,14 @@ public class MediaGalleryActivity extends ThreemaToolbarActivity implements Adap
 	private SpinnerMessageFilter spinnerMessageFilter;
 	private MediaGallerySpinnerAdapter spinnerAdapter;
 	private List<AbstractMessageModel> values;
-	private GridView gridView;
+	private FastScrollGridView gridView;
 	private EmptyView emptyView;
 	private TypedArray mediaTypeArray;
 	private int currentType;
 	private ActionMode actionMode = null;
 	private AbstractMessageModel initialMessageModel = null;
+	private TextView dateTextView;
+	private FrameLayout dateView;
 
 	public FileService fileService;
 	public MessageService messageService;
@@ -109,6 +117,13 @@ public class MediaGalleryActivity extends ThreemaToolbarActivity implements Adap
 	public GroupService groupService;
 	public DistributionListService distributionListService;
 
+	private final Handler dateViewHandler = new Handler();
+	private final Runnable dateViewTask = () -> RuntimeUtil.runOnUiThread(() -> {
+		if (dateView != null && dateView.getVisibility() == View.VISIBLE) {
+			AnimationUtil.slideOutAnimation(dateView, false, 1f, null);
+		}
+	});
+
 	private final int TYPE_ALL = 0;
 	private final int TYPE_IMAGE = 1;
 	private final int TYPE_VIDEO = 2;
@@ -196,6 +211,10 @@ public class MediaGalleryActivity extends ThreemaToolbarActivity implements Adap
 	@Override
 	protected boolean initActivity(Bundle savedInstanceState) {
 		logger.debug("initActivity");
+
+		// set font size according to user preferences
+		getTheme().applyStyle(preferenceService.getFontStyle(), true);
+
 		if (!super.initActivity(savedInstanceState)) {
 			return false;
 		}
@@ -265,6 +284,7 @@ public class MediaGalleryActivity extends ThreemaToolbarActivity implements Adap
 		});
 		this.gridView.setOnItemClickListener(this);
 		this.gridView.setNumColumns(ConfigUtils.isLandscape(this) ? 5 : 3);
+		this.gridView.setScrollListener(this);
 
 		processIntent(getIntent());
 
@@ -300,6 +320,9 @@ public class MediaGalleryActivity extends ThreemaToolbarActivity implements Adap
 		frameLayout.addView(this.emptyView);
 		this.gridView.setEmptyView(this.emptyView);
 
+		this.dateView = findViewById(R.id.date_separator_container);
+		this.dateTextView = findViewById(R.id.text_view);
+
 		if (savedInstanceState == null || mediaGalleryAdapter == null) {
 			setupAdapters(this.currentType, true);
 		}
@@ -469,6 +492,28 @@ public class MediaGalleryActivity extends ThreemaToolbarActivity implements Adap
 		return true;
 	}
 
+	@Override
+	public void onScroll(int firstVisibleItem) {
+		if (this.mediaGalleryAdapter != null) {
+			if (dateView.getVisibility() != View.VISIBLE && mediaGalleryAdapter != null && mediaGalleryAdapter.getCount() > 0) {
+				AnimationUtil.slideInAnimation(dateView, false, 200);
+			}
+
+			dateViewHandler.removeCallbacks(dateViewTask);
+			dateViewHandler.postDelayed(dateViewTask, SCROLLBUTTON_VIEW_TIMEOUT);
+
+			final AbstractMessageModel messageModel = this.mediaGalleryAdapter.getItem(firstVisibleItem);
+			if (messageModel != null) {
+				final Date createdAt = messageModel.getCreatedAt();
+				if (createdAt != null) {
+					dateView.post(() -> {
+						dateTextView.setText(LocaleUtil.formatDateRelative(this, createdAt.getTime()));
+					});
+				}
+			}
+		}
+	}
+
 	private void selectAllMessages() {
 		if (gridView != null) {
 			if (gridView.getCount() == gridView.getCheckedItemCount()) {

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

@@ -724,7 +724,7 @@ public class SendMediaActivity extends ThreemaToolbarActivity implements
 		}
 
 		int oldRotation = SendMediaActivity.this.mediaItems.get(bigImagePos).getRotation();
-		int newRotation = (oldRotation + 90) % 360;
+		int newRotation = (oldRotation - 90) % 360;
 
 		int height = bigImageView.getDrawable().getBounds().width();
 		int width = bigImageView.getDrawable().getBounds().height();
@@ -739,7 +739,7 @@ public class SendMediaActivity extends ThreemaToolbarActivity implements
 			scalingFactor = (float) parentWidth / (float) width;
 		}
 
-		bigImageView.animate().rotationBy(90f)
+		bigImageView.animate().rotationBy(-90f)
 			.scaleX(scalingFactor)
 			.scaleY(scalingFactor)
 			.setDuration(IMAGE_ANIMATION_DURATION_MS)

+ 32 - 24
app/src/main/java/ch/threema/app/activities/wizard/WizardStartActivity.java

@@ -49,7 +49,28 @@ public class WizardStartActivity extends WizardBackgroundActivity {
 		setContentView(R.layout.activity_wizard_start);
 
 		final ImageView imageView = findViewById(R.id.wizard_animation);
-		imageView.setBackgroundResource(R.drawable.animation_wizard1);
+		final AnimationDrawable frameAnimation = (AnimationDrawable) imageView.getBackground();
+		frameAnimation.setOneShot(true);
+		frameAnimation.setCallback(new AnimationDrawableCallback(frameAnimation, imageView) {
+			@Override
+			public void onAnimationAdvanced(int currentFrame, int totalFrames) {
+			}
+
+			@Override
+			public void onAnimationCompleted() {
+				ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(
+					// the context of the activity
+					WizardStartActivity.this,
+
+					new Pair<>(findViewById(R.id.wizard_animation),
+						getString(R.string.transition_name_dots)),
+					new Pair<>(findViewById(R.id.wizard_footer),
+						getString(R.string.transition_name_logo))
+				);
+				launchNextActivity(options);
+			}
+		});
+
 		if (!RuntimeUtil.isInTest() && !ConfigUtils.isWorkRestricted()) {
 			imageView.setOnClickListener(new View.OnClickListener() {
 				@Override
@@ -58,30 +79,17 @@ public class WizardStartActivity extends WizardBackgroundActivity {
 					launchNextActivity(null);
 				}
 			});
-			imageView.getRootView().getViewTreeObserver().addOnGlobalLayoutListener(() -> {
-				AnimationDrawable frameAnimation = (AnimationDrawable) imageView.getBackground();
-				frameAnimation.setOneShot(true);
-				frameAnimation.setCallback(new AnimationDrawableCallback(frameAnimation, imageView) {
-					@Override
-					public void onAnimationAdvanced(int currentFrame, int totalFrames) {
-					}
-
-					@Override
-					public void onAnimationCompleted() {
-						ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(
-							// the context of the activity
-							WizardStartActivity.this,
-
-							new Pair<>(findViewById(R.id.wizard_animation),
-								getString(R.string.transition_name_dots)),
-							new Pair<>(findViewById(R.id.wizard_footer),
-								getString(R.string.transition_name_logo))
-						);
-						launchNextActivity(options);
+			imageView.getRootView().getViewTreeObserver().addOnGlobalLayoutListener(frameAnimation::start);
+			imageView.postDelayed(new Runnable() {
+				@Override
+				public void run() {
+					if (frameAnimation.isRunning()) {
+						// stop animation if it's still running after 5 seconds
+						frameAnimation.stop();
+						launchNextActivity(null);
 					}
-				});
-				frameAnimation.start();
-			});
+				}
+			}, 5000);
 		} else {
 			launchNextActivity(null);
 		}

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

@@ -833,6 +833,10 @@ public class ComposeMessageAdapter extends ArrayAdapter<AbstractMessageModel> {
 		return Integer.MAX_VALUE;
 	}
 
+	public void setUnreadMessagesCount(int unreadMessagesCount) {
+		this.unreadMessagesCount = unreadMessagesCount;
+	}
+
 	public boolean removeFirstUnreadPosition() {
 		if(this.firstUnreadPos >= 0) {
 			if(this.firstUnreadPos >= this.getCount()) {

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

@@ -72,7 +72,6 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
 	private final ContactService contactService;
 	private final PreferenceService preferenceService;
 	private final IdListService blackListIdentityService;
-	private final Context context;
 
 	public static final int VIEW_TYPE_NORMAL = 0;
 	public static final int VIEW_TYPE_COUNT = 2;
@@ -105,7 +104,6 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
 		this.values = updateRecentlyAdded(values);
 		this.ovalues = this.values;
 		this.contactService = contactService;
-		this.context = context;
 		this.preferenceService = preferenceService;
 		this.blackListIdentityService = blackListIdentityService;
 		this.defaultContactImage = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_contact);

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

@@ -121,6 +121,7 @@ public class DirectoryAdapter extends PagedListAdapter<WorkDirectoryContact, Rec
 
 	@Override
 	public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
+		boolean isMe = false;
 		final DirectoryHolder holder = (DirectoryHolder) viewHolder;
 
 		final WorkDirectoryContact workDirectoryContact = this.getItem(position);
@@ -130,15 +131,20 @@ public class DirectoryAdapter extends PagedListAdapter<WorkDirectoryContact, Rec
 		}
 
 		holder.contact = workDirectoryContact;
+		if (holder.contact != null) {
+			isMe = holder.contact.threemaId.equals(contactService.getMe().getIdentity());
+		}
 
 		if (this.onClickItemListener != null) {
-			holder.statusImageView.setOnClickListener(new View.OnClickListener() {
-				@Override
-				public void onClick(View v) {
-					onClickItemListener.onAdd(holder.contact, viewHolder.getAdapterPosition());
-				}
-			});
-			holder.itemView.setOnClickListener(v -> onClickItemListener.onClick(holder.contact, viewHolder.getAdapterPosition()));
+			if (!isMe) {
+				holder.statusImageView.setOnClickListener(new View.OnClickListener() {
+					@Override
+					public void onClick(View v) {
+						onClickItemListener.onAdd(holder.contact, viewHolder.getAdapterPosition());
+					}
+				});
+				holder.itemView.setOnClickListener(v -> onClickItemListener.onClick(holder.contact, viewHolder.getAdapterPosition()));
+			}
 		}
 
 		String name;
@@ -184,9 +190,10 @@ public class DirectoryAdapter extends PagedListAdapter<WorkDirectoryContact, Rec
 		boolean isAddedContact = contactService.getByIdentity(workDirectoryContact.threemaId) != null;
 
 		holder.statusImageView.setBackgroundResource(isAddedContact ? 0 : this.backgroundRes);
-		holder.statusImageView.setImageResource(isAddedContact ? R.drawable.ic_keyboard_arrow_right_black_24dp : R.drawable.ic_add_circle_outline_black_24dp);
-		holder.statusImageView.setClickable(!isAddedContact);
-		holder.statusImageView.setFocusable(!isAddedContact);
+		holder.statusImageView.setImageResource(isMe ? R.drawable.ic_person_outline : (isAddedContact ? R.drawable.ic_keyboard_arrow_right_black_24dp : R.drawable.ic_add_circle_outline_black_24dp));
+		holder.statusImageView.setContentDescription(context.getString(isMe ? R.string.me_myself_and_i : (isAddedContact ? R.string.title_compose_message : R.string.menu_add_contact)));
+		holder.statusImageView.setClickable(!isAddedContact && !isMe);
+		holder.statusImageView.setFocusable(!isAddedContact && !isMe);
 	}
 
 	private static final DiffUtil.ItemCallback<WorkDirectoryContact> DIFF_CALLBACK =

+ 7 - 9
app/src/main/java/ch/threema/app/adapters/MediaGalleryAdapter.java

@@ -51,7 +51,6 @@ import ch.threema.app.ui.SquareImageView;
 import ch.threema.app.ui.listitemholder.AbstractListItemHolder;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.FileUtil;
-import ch.threema.app.utils.MessageUtil;
 import ch.threema.app.utils.StringConversionUtil;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.MessageType;
@@ -98,8 +97,8 @@ public class MediaGalleryAdapter extends ArrayAdapter<AbstractMessageModel> {
 		public ImageView imageView;
 		public ControllerView playButton;
 		public ProgressBar progressBar;
-		public TextView textView;
 		public TextView topTextView;
+		public View textContainerView;
 		public int messageId;
 	}
 
@@ -181,7 +180,7 @@ public class MediaGalleryAdapter extends ArrayAdapter<AbstractMessageModel> {
 							boolean broken = false;
 
 							if (thumbnail != null && !thumbnail.isRecycled()) {
-								holder.topTextView.setVisibility(View.GONE);
+								holder.textContainerView.setVisibility(View.GONE);
 								holder.imageView.setImageBitmap(thumbnail);
 								holder.imageView.clearColorFilter();
 								holder.imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
@@ -190,7 +189,7 @@ public class MediaGalleryAdapter extends ArrayAdapter<AbstractMessageModel> {
 									// try default avatar for mime type
 									thumbnail = fileService.getDefaultMessageThumbnailBitmap(getContext(), messageModel, null, messageModel.getFileData().getMimeType());
 									holder.topTextView.setText(messageModel.getFileData().getFileName());
-									holder.topTextView.setVisibility(View.VISIBLE);
+									holder.textContainerView.setVisibility(View.VISIBLE);
 									if (thumbnail != null) {
 										holder.imageView.setScaleType(ImageView.ScaleType.CENTER);
 										holder.imageView.setImageBitmap(thumbnail);
@@ -203,9 +202,9 @@ public class MediaGalleryAdapter extends ArrayAdapter<AbstractMessageModel> {
 									holder.imageView.setImageResource(R.drawable.ic_keyboard_voice_outline);
 									holder.imageView.setColorFilter(foregroundColor, PorterDuff.Mode.SRC_IN);
 									holder.topTextView.setText(StringConversionUtil.secondsToString((long) (messageModel.getAudioData().getDuration()), false));
-									holder.topTextView.setVisibility(View.VISIBLE);
+									holder.textContainerView.setVisibility(View.VISIBLE);
 								} else {
-									holder.topTextView.setVisibility(View.GONE);
+									holder.textContainerView.setVisibility(View.GONE);
 									broken = true;
 								}
 							}
@@ -252,14 +251,14 @@ public class MediaGalleryAdapter extends ArrayAdapter<AbstractMessageModel> {
 			SquareImageView imageView = itemView.findViewById(R.id.image_view);
 			ControllerView playButton = itemView.findViewById(R.id.play_button);
 			ProgressBar progressBar = itemView.findViewById(R.id.progress_decoding);
-			TextView textView = itemView.findViewById(R.id.text);
 			TextView topTextView = itemView.findViewById(R.id.text_filename);
+			View textContainerView = itemView.findViewById(R.id.filename_container);
 
 			holder.imageView = imageView;
 			holder.playButton = playButton;
 			holder.progressBar = progressBar;
-			holder.textView = textView;
 			holder.topTextView = topTextView;
+			holder.textContainerView = textContainerView;
 			holder.messageId = 0;
 
 			itemView.setTag(holder);
@@ -274,7 +273,6 @@ public class MediaGalleryAdapter extends ArrayAdapter<AbstractMessageModel> {
 		if (holder.messageId != messageModel.getId()) {
 			// do not load contents again if it's unchanged
 			this.loadThumbnailBitmap(position, holder, messageModel);
-			holder.textView.setText(MessageUtil.getDisplayDate(this.getContext(), messageModel, true));
 			if (this.brokenThumbnails.contains(messageModel.getId())) {
 				holder.playButton.setBroken();
 				holder.playButton.setVisibility(View.VISIBLE);

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

@@ -29,7 +29,7 @@ import ch.threema.app.utils.TestUtil;
 import ch.threema.storage.models.AbstractMessageModel;
 
 public class FirstUnreadChatAdapterDecorator extends ChatAdapterDecorator {
-	private int unreadMessagesCount = 0;
+	private int unreadMessagesCount;
 
 	public FirstUnreadChatAdapterDecorator(Context context, AbstractMessageModel messageModel, Helper helper, final int unreadMessagesCount) {
 		super(context, messageModel, helper);
@@ -39,10 +39,6 @@ public class FirstUnreadChatAdapterDecorator extends ChatAdapterDecorator {
 
 	@Override
 	protected void configureChatMessage(final ComposeMessageHolder holder, final int position) {
-		if (this.unreadMessagesCount < 1) {
-			return;
-		}
-
 		String s;
 		if (this.unreadMessagesCount > 1) {
 			s = getContext().getString(R.string.unread_messages, unreadMessagesCount);

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

@@ -24,8 +24,6 @@ package ch.threema.app.asynctasks;
 import android.os.AsyncTask;
 import android.widget.Toast;
 
-import com.google.firebase.iid.FirebaseInstanceId;
-
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -33,6 +31,7 @@ import java.io.File;
 import java.io.IOException;
 
 import androidx.fragment.app.FragmentManager;
+import ch.threema.app.push.PushService;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.dialogs.GenericProgressDialog;
@@ -42,8 +41,6 @@ import ch.threema.app.utils.DialogUtil;
 import ch.threema.app.utils.SecureDeleteUtil;
 import ch.threema.app.webclient.services.SessionWakeUpServiceImpl;
 import ch.threema.app.webclient.services.instance.DisconnectContext;
-import ch.threema.base.ThreemaException;
-import ch.threema.client.ProtocolDefines;
 import ch.threema.client.ThreemaConnection;
 import ch.threema.storage.DatabaseServiceNew;
 import ch.threema.storage.NonceDatabaseBlobService;
@@ -76,15 +73,7 @@ public class DeleteIdentityAsyncTask extends AsyncTask<Void, Void, Exception> {
 
 		try {
 			// clear push token
-			FirebaseInstanceId.getInstance().deleteInstanceId();
-			if (connection != null) {
-				try {
-					connection.setPushToken(ProtocolDefines.PUSHTOKEN_TYPE_GCM, "");
-				} catch (ThreemaException e) {
-					// ignore inability to clear push token due to missing connectivity
-					logger.debug("Exception", e);
-				}
-			}
+			PushService.deleteToken(ThreemaApplication.getAppContext());
 
 			serviceManager.getThreemaSafeService().unscheduleUpload();
 			serviceManager.getMessageService().removeAll();

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

@@ -204,13 +204,20 @@ public class BackupService extends Service {
 				Date now = new Date();
 				DocumentFile zipFile = null;
 				Uri backupUri = this.fileService.getBackupUri();
+
+				if (backupUri == null) {
+					showBackupErrorNotification("Destination directory has not been selected yet");
+					stopSelf();
+					return START_NOT_STICKY;
+				}
+
 				String filename = "threema-backup_" + userService.getIdentity() + "_" + now.getTime() + "_1";
 
 				if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(backupUri.getScheme())) {
 					zipFile = DocumentFile.fromFile(new File(backupUri.getPath(), INCOMPLETE_BACKUP_FILENAME_PREFIX + filename + ".zip"));
 					success = true;
 				} else {
-					DocumentFile directory = DocumentFile.fromTreeUri(getApplicationContext(), this.fileService.getBackupUri());
+					DocumentFile directory = DocumentFile.fromTreeUri(getApplicationContext(), backupUri);
 					if (directory != null && directory.exists()) {
 						try {
 							zipFile = directory.createFile(MimeUtil.MIME_TYPE_ZIP, INCOMPLETE_BACKUP_FILENAME_PREFIX + filename);

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

@@ -1402,6 +1402,8 @@ public class RestoreService extends Service {
 			state = MessageState.SENDING;
 		} else if (messageState.equals(MessageState.SENT.name())) {
 			state = MessageState.SENT;
+		} else if (messageState.equals(MessageState.CONSUMED.name())) {
+			state = MessageState.CONSUMED;
 		}
 
 		messageModel.setState(state);

+ 14 - 9
app/src/main/java/ch/threema/app/camera/CameraFragment.java

@@ -29,8 +29,6 @@ import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
-import android.graphics.Color;
-import android.graphics.drawable.ColorDrawable;
 import android.hardware.display.DisplayManager;
 import android.os.AsyncTask;
 import android.os.Build;
@@ -87,6 +85,7 @@ import static android.view.Surface.ROTATION_180;
 import static ch.threema.app.camera.CameraActivity.KEY_EVENT_ACTION;
 import static ch.threema.app.camera.CameraActivity.KEY_EVENT_EXTRA;
 
+@SuppressWarnings("deprecation")
 @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
 public class CameraFragment extends Fragment {
 	private static final Logger logger = LoggerFactory.getLogger(CameraFragment.class);
@@ -222,7 +221,7 @@ public class CameraFragment extends Fragment {
 		}
 	};
 
-	@SuppressLint("UnsafeExperimentalUsageError")
+	@SuppressLint("UnsafeOptInUsageError")
 	private final OnVideoSavedCallback onVideoSavedCallback = new OnVideoSavedCallback() {
 		@SuppressLint("StaticFieldLeak")
 		@Override
@@ -479,10 +478,16 @@ public class CameraFragment extends Fragment {
 				}
 
 				// play shutter sound
-				if (mediaActionSound != null) {
-					mediaActionSound.play(LessObnoxiousMediaActionSound.SHUTTER_CLICK);
-				}
+				cameraView.postDelayed(new Runnable() {
+					@Override
+					public void run() {
+						if (mediaActionSound != null) {
+							mediaActionSound.play(LessObnoxiousMediaActionSound.SHUTTER_CLICK);
+						}
+					}
+				}, 100);
 
+/*
 				// We can only change the foreground Drawable using API level 23+ API
 				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 					// Display flash animation to indicate that photo was captured
@@ -505,7 +510,7 @@ public class CameraFragment extends Fragment {
 						}
 					}, cameraView.getFlash() == ImageCapture.FLASH_MODE_ON ? 1000 : 100);
 				}
-			}
+*/			}
 		});
 
 		TextView shutterExplainText = controlsContainer.findViewById(R.id.shutter_explain);
@@ -601,7 +606,7 @@ public class CameraFragment extends Fragment {
 		}
 	}
 
-	@SuppressLint("UnsafeExperimentalUsageError")
+	@SuppressLint({"UnsafeExperimentalUsageError", "UnsafeOptInUsageError"})
 	private void startVideoRecording() {
 		// play shutter sound
 		if (mediaActionSound != null) {
@@ -624,7 +629,7 @@ public class CameraFragment extends Fragment {
 
 	}
 
-	@SuppressLint("UnsafeExperimentalUsageError")
+	@SuppressLint({"UnsafeExperimentalUsageError", "UnsafeOptInUsageError"})
 	private void stopVideoRecording() {
 		if (timerView != null) {
 			timerView.stop();

+ 6 - 0
app/src/main/java/ch/threema/app/camera/CameraView.java

@@ -89,6 +89,7 @@ import androidx.camera.core.impl.LensFacingConverter;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.impl.utils.futures.FutureCallback;
 import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.camera.view.LifecycleCameraController;
 import androidx.camera.view.PreviewView;
 import androidx.camera.view.video.ExperimentalVideo;
 import androidx.camera.view.video.OnVideoSavedCallback;
@@ -113,7 +114,12 @@ import ch.threema.app.utils.ConfigUtils;
  * <p>Because the Camera is a limited resource and consumes a high amount of power, CameraView must
  * be opened/closed. CameraView will handle opening/closing automatically through use of a {@link
  * LifecycleOwner}. Use {@link #bindToLifecycle(LifecycleOwner)} to start the camera.
+ *
+ * @deprecated Use {@link LifecycleCameraController}. See
+ * <a href="https://medium.com/androiddevelopers/camerax-learn-how-to-use-cameracontroller
+ * -e3ed10fffecf">migration guide</a>.
  */
+@Deprecated
 @SuppressLint("RestrictedApi")
 @TargetApi(21)
 public final class CameraView extends FrameLayout {

+ 13 - 5
app/src/main/java/ch/threema/app/camera/CameraXModule.java

@@ -61,8 +61,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RequiresPermission;
-import androidx.annotation.experimental.UseExperimental;
 import androidx.camera.core.Camera;
 import androidx.camera.core.CameraInfoUnavailableException;
 import androidx.camera.core.CameraSelector;
@@ -80,6 +80,7 @@ import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.impl.utils.futures.FutureCallback;
 import androidx.camera.core.impl.utils.futures.Futures;
 import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.camera.view.LifecycleCameraController;
 import androidx.camera.view.PreviewView;
 import androidx.camera.view.video.ExperimentalVideo;
 import androidx.core.util.Preconditions;
@@ -91,7 +92,14 @@ import ch.threema.app.utils.ConfigUtils;
 
 import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
 
-/** CameraX use case operation built on @{link androidx.camera.core}. */
+/**
+ * CameraX use case operation built on @{link androidx.camera.core}.
+ *
+ * @deprecated Use {@link LifecycleCameraController}. See
+ * <a href="https://medium.com/androiddevelopers/camerax-learn-how-to-use-cameracontroller
+ * -e3ed10fffecf">migration guide</a>.
+ */
+@Deprecated
 @SuppressLint("RestrictedApi")
 @TargetApi(21)
 final class CameraXModule {
@@ -189,7 +197,7 @@ final class CameraXModule {
         }
     }
 
-    @UseExperimental(markerClass = ExperimentalVideo.class)
+    @OptIn(markerClass = ExperimentalVideo.class)
     @RequiresPermission(permission.CAMERA)
     void bindToLifecycleAfterViewMeasured() {
         if (mNewLifecycle == null) {
@@ -347,7 +355,7 @@ final class CameraXModule {
                 "Explicit open/close of camera not yet supported. Use bindtoLifecycle() instead.");
     }
 
-    @UseExperimental(markerClass = ExperimentalVideo.class)
+    @OptIn(markerClass = ExperimentalVideo.class)
     public void takePicture(Executor executor, OnImageCapturedCallback callback) {
         if (mImageCapture == null) {
             return;
@@ -364,7 +372,7 @@ final class CameraXModule {
         mImageCapture.takePicture(executor, callback);
     }
 
-    @UseExperimental(markerClass = ExperimentalVideo.class)
+    @OptIn(markerClass = ExperimentalVideo.class)
     public void takePicture(@NonNull ImageCapture.OutputFileOptions outputFileOptions,
             @NonNull Executor executor, OnImageSavedCallback callback) {
         if (mImageCapture == null) {

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

@@ -223,6 +223,9 @@ public class MessageDetailDialog extends ThreemaDialogFragment {
 				case TRANSCODING:
 					stateResource = R.string.state_transcoding;
 					break;
+				case CONSUMED:
+					stateResource = R.string.listened_to;
+					break;
 			}
 		} else {
 			stateResource = R.string.state_sent;

+ 30 - 4
app/src/main/java/ch/threema/app/fragments/BackupDataFragment.java

@@ -85,6 +85,7 @@ public class BackupDataFragment extends Fragment implements
 	private static final String DIALOG_TAG_ENERGY_SAVING_REMINDER = "esr";
 	private static final String DIALOG_TAG_DISABLE_ENERGY_SAVING = "des";
 	private static final String DIALOG_TAG_PASSWORD = "pwd";
+	private static final String DIALOG_TAG_PATH_INTRO = "pathintro";
 
 	private BackupRestoreDataService backupRestoreDataService;
 	private View fragmentView;
@@ -97,6 +98,8 @@ public class BackupDataFragment extends Fragment implements
 	private NestedScrollView scrollView;
 	private MaterialButton pathChangeButton;
 
+	private boolean launchedFromFAB = false;
+
 	@Override
 	public void onDestroyView() {
 		this.fab.setOnClickListener(null);
@@ -144,6 +147,7 @@ public class BackupDataFragment extends Fragment implements
 			fab.setOnClickListener(new View.OnClickListener() {
 				@Override
 				public void onClick(View v) {
+					launchedFromFAB = true;
 					initiateBackup();
 				}
 			});
@@ -168,12 +172,16 @@ public class BackupDataFragment extends Fragment implements
 			pathChangeButton.findViewById(R.id.backup_path_change_btn).setOnClickListener(new View.OnClickListener() {
 				@Override
 				public void onClick(View v) {
-					onPathChangeButtonClicked(v);
+					launchedFromFAB = false;
+					showPathSelectionIntro();
 				}
 			});
 
 			pathTextView = fragmentView.findViewById(R.id.backup_path);
-			pathTextView.setText(backupUri.toString());
+			pathTextView.setText(
+				backupUri == null ?
+				getString(R.string.not_set) :
+				backupUri.toString());
 		}
 
 		Date backupDate = preferenceService.getLastDataBackupDate();
@@ -189,7 +197,13 @@ public class BackupDataFragment extends Fragment implements
 		return this.fragmentView;
 	}
 
-	private void onPathChangeButtonClicked(View v) {
+	private void showPathSelectionIntro() {
+		GenericAlertDialog dialog = GenericAlertDialog.newInstance(R.string.set_backup_path, R.string.set_backup_path_intro, R.string.ok, R.string.cancel);
+		dialog.setTargetFragment(this);
+		dialog.show(getFragmentManager(), DIALOG_TAG_PATH_INTRO);
+	}
+
+	private void launchDocumentTree() {
 		if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
 			try {
 				Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
@@ -217,7 +231,11 @@ public class BackupDataFragment extends Fragment implements
 			//show toast
 			Toast.makeText(ThreemaApplication.getAppContext(), R.string.backup_in_progress, Toast.LENGTH_SHORT).show();
 		} else {
-			checkBatteryOptimizations();
+			if (backupUri == null) {
+				showPathSelectionIntro();
+			} else {
+				checkBatteryOptimizations();
+			}
 		}
 	}
 
@@ -283,6 +301,9 @@ public class BackupDataFragment extends Fragment implements
 			case DIALOG_TAG_ENERGY_SAVING_REMINDER:
 				doBackup();
 				break;
+			case DIALOG_TAG_PATH_INTRO:
+				launchDocumentTree();
+				break;
 			default:
 				break;
 		}
@@ -368,6 +389,11 @@ public class BackupDataFragment extends Fragment implements
 							backupUri = treeUri;
 							preferenceService.setDataBackupUri(treeUri);
 							pathTextView.setText(treeUri.toString());
+
+							if (launchedFromFAB) {
+								checkBatteryOptimizations();
+							}
+
 							return;
 						}
 					}

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

@@ -285,7 +285,7 @@ public class ComposeMessageFragment extends Fragment implements
 
 	public static final long VIBRATION_MSEC = 300;
 	private static final long MESSAGE_PAGE_SIZE = 100;
-	private static final int SCROLLBUTTON_VIEW_TIMEOUT = 3000;
+	public static final int SCROLLBUTTON_VIEW_TIMEOUT = 3000;
 	private static final int SMOOTHSCROLL_THRESHOLD = 10;
 	private static final int MAX_SELECTED_ITEMS = 100; // may not be larger than MESSAGE_PAGE_SIZE
 	private static final int MAX_FORWARDABLE_ITEMS = 50;
@@ -1319,6 +1319,13 @@ public class ComposeMessageFragment extends Fragment implements
 		if(this.typingIndicatorTextWatcher != null) {
 			this.typingIndicatorTextWatcher.stopTyping();
 		}
+
+		if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
+			// close keyboard to prevent layout corruption after unlocking phone
+			if (this.messageText != null) {
+				EditTextUtil.hideSoftKeyboard(this.messageText);
+			}
+		}
 		super.onStop();
 	}
 
@@ -2550,6 +2557,7 @@ public class ComposeMessageFragment extends Fragment implements
 			this.composeMessageAdapter.setThumbnailWidth(ConfigUtils.getPreferredThumbnailWidth(getContext(), false));
 			this.composeMessageAdapter.setGroupId(groupId);
 			this.composeMessageAdapter.setMessageReceiver(this.messageReceiver);
+			this.composeMessageAdapter.setUnreadMessagesCount(unreadCount);
 			this.insertToList(values, true, true, true);
 			updateToolbarTitle();
 		} else {

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

@@ -216,7 +216,6 @@ public class MessageSectionFragment extends MainFragment
 	private int cornerRadius;
 	private TagModel unreadTagModel;
 
-
 	private int archiveCount = 0;
 	private Snackbar archiveSnackbar;
 
@@ -1042,9 +1041,9 @@ public class MessageSectionFragment extends MainFragment
 			//
 			if(!this.requiredInstances()) {
 				logger.error("could not instantiate required objects");
+			} else {
+				this.unreadTagModel = this.conversationTagService.getTagModel(ConversationTagServiceImpl.FIXED_TAG_UNREAD);
 			}
-
-			this.unreadTagModel = this.conversationTagService.getTagModel(ConversationTagServiceImpl.FIXED_TAG_UNREAD);
 		}
 		return fragmentView;
 	}

+ 0 - 53
app/src/main/java/ch/threema/app/jobs/FcmRegistrationJobService.java

@@ -1,53 +0,0 @@
-/*  _____ _
- * |_   _| |_  _ _ ___ ___ _ __  __ _
- *   | | | ' \| '_/ -_) -_) '  \/ _` |_
- *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
- *
- * Threema for Android
- * Copyright (c) 2018-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.jobs;
-
-import android.app.job.JobParameters;
-import android.app.job.JobService;
-import android.content.Intent;
-import android.os.Build;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import androidx.annotation.RequiresApi;
-import ch.threema.app.FcmRegistrationIntentService;
-
-@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
-public class FcmRegistrationJobService extends JobService {
-	private static final Logger logger = LoggerFactory.getLogger(FcmRegistrationJobService.class);
-
-	@Override
-	public boolean onStartJob(JobParameters params) {
-		logger.debug("onStartJob");
-
-		FcmRegistrationIntentService.enqueueWork(getApplicationContext(), new Intent());
-
-		// work has been queued, we no longer need this job
-		return false;
-	}
-
-	@Override
-	public boolean onStopJob(JobParameters params) {
-		return false;
-	}
-}

BIN
app/src/main/java/ch/threema/app/libs/agcp-1.4.2.300.jar


BIN
app/src/main/java/ch/threema/app/libs/push-5.0.4.302.aar


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

@@ -524,13 +524,13 @@ public class ServiceManager {
 							DeviceIdUtil.getDeviceId(getContext()));
 					break;
 				case GOOGLE_WORK:
+				case HMS_WORK:
 					this.licenseService = new LicenseServiceUser(
-							this.getAPIConnector(),
-							this.getPreferenceService(),
-							DeviceIdUtil.getDeviceId(getContext()));
+						this.getAPIConnector(),
+						this.getPreferenceService(),
+						DeviceIdUtil.getDeviceId(getContext()));
 					break;
 				default:
-					//TODO implement LVL
 					this.licenseService = new LicenseService() {
 						@Override
 						public String validate(Credentials credentials) {
@@ -645,7 +645,6 @@ public class ServiceManager {
 	public ConversationTagService getConversationTagService() {
 		if(null == this.conversationService) {
 			this.conversationTagService = new ConversationTagServiceImpl(
-				this.getContext(),
 				this.databaseServiceNew
 			);
 		}
@@ -675,9 +674,7 @@ public class ServiceManager {
 			this.notificationService = new NotificationServiceImpl(
 					this.getContext(),
 					this.getLockAppService(),
-					this.getMutedChatsListService(),
 					this.getHiddenChatsListService(),
-					this.getMentionOnlyChatsListService(),
 					this.getPreferenceService(),
 					this.getRingtoneService()
 			);

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

@@ -241,7 +241,7 @@ public class SettingsAboutFragment extends ThreemaPreferenceFragment {
 				new AsyncTask<Void, Void, String>() {
 					@Override
 					protected void onPreExecute() {
-						GenericProgressDialog.newInstance(R.string.check_updates, R.string.please_wait).show(getFragmentManager(), DIALOG_TAG_CHECK_UPDATE);
+						GenericProgressDialog.newInstance(R.string.check_updates, R.string.please_wait).show(getActivity().getSupportFragmentManager(), DIALOG_TAG_CHECK_UPDATE);
 					}
 
 					@Override
@@ -268,9 +268,9 @@ public class SettingsAboutFragment extends ThreemaPreferenceFragment {
 
 					@Override
 					protected void onPostExecute(String error) {
-						DialogUtil.dismissDialog(getFragmentManager(), DIALOG_TAG_CHECK_UPDATE, true);
+						DialogUtil.dismissDialog(getParentFragmentManager(), DIALOG_TAG_CHECK_UPDATE, true);
 						if (error != null) {
-							SimpleStringAlertDialog.newInstance(R.string.check_updates, error).show(getFragmentManager(), "nu");
+							SimpleStringAlertDialog.newInstance(R.string.check_updates, error).show(getParentFragmentManager(), "nu");
 						} else {
 							Intent dialogIntent = IntentDataUtil.createActionIntentUpdateAvailable(updateMessage, updateUrl);
 							dialogIntent.putExtra(DownloadApkActivity.EXTRA_FORCE_UPDATE_DIALOG, true);

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

@@ -54,13 +54,12 @@ import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
 import androidx.preference.DropDownPreference;
-import androidx.preference.Preference.SummaryProvider;
 import androidx.preference.Preference;
+import androidx.preference.Preference.SummaryProvider;
 import androidx.preference.PreferenceCategory;
 import androidx.preference.PreferenceScreen;
 import androidx.preference.TwoStatePreference;
 import ch.threema.app.BuildConfig;
-import ch.threema.app.FcmRegistrationIntentService;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.activities.DisableBatteryOptimizationsActivity;
@@ -73,6 +72,7 @@ import ch.threema.app.listeners.ConversationListener;
 import ch.threema.app.managers.ListenerManager;
 import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.messagereceiver.MessageReceiver;
+import ch.threema.app.push.PushService;
 import ch.threema.app.services.ContactService;
 import ch.threema.app.services.DeadlineListService;
 import ch.threema.app.services.FileService;
@@ -105,8 +105,8 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 	private static final Logger logger = LoggerFactory.getLogger(SettingsTroubleshootingFragment.class);
 
 	private static final String DIALOG_TAG_REMOVE_WALLPAPERS = "removeWP";
-	private static final String DIALOG_TAG_GCM_REGISTER = "gcmReg";
-	private static final String DIALOG_TAG_GCM_RESULT = "gcmRes";
+	private static final String DIALOG_TAG_PUSH_REGISTER = "pushReg";
+	private static final String DIALOG_TAG_PUSH_RESULT = "pushRes";
 	private static final String DIALOG_TAG_RESET_RINGTONES = "rri";
 	private static final String DIALOG_TAG_IPV6_APP_RESTART = "rs";
 	private static final String DIALOG_TAG_POWERMANAGER_WORKAROUNDS = "hw";
@@ -137,10 +137,10 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 	private MessageService messageService;
 	private ContactService contactService;
 
-	private BroadcastReceiver gcmRegisterBroadcastReceiver;
+	private BroadcastReceiver pushTokenResetBroadcastReceiver;
 	private View fragmentView;
 
-	private boolean isPlayServicesInstalled;
+	private boolean pushServicesInstalled;
 
 	@Override
 	public void onCreatePreferencesFix(@Nullable Bundle savedInstanceState, String rootKey) {
@@ -153,40 +153,44 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 		PreferenceScreen preferenceScreen = (PreferenceScreen) findPreference("pref_key_troubleshooting");
 
 		sharedPreferences = getPreferenceManager().getSharedPreferences();
+		pushServicesInstalled = PushService.servicesInstalled(getContext());
 
-		gcmRegisterBroadcastReceiver = new BroadcastReceiver() {
+		pushTokenResetBroadcastReceiver = new BroadcastReceiver() {
 			// register listener for gcm registration result
 			@Override
 			public void onReceive(Context context, Intent intent) {
-				DialogUtil.dismissDialog(getFragmentManager(), DIALOG_TAG_GCM_REGISTER, true);
-				boolean sentToken = !PushUtil.pushTokenNeedsRefresh(context);
-
-				SimpleStringAlertDialog.newInstance(-1, sentToken ?
-						(intent.getBooleanExtra(FcmRegistrationIntentService.EXTRA_CLEAR_TOKEN, false) ?
-								getString(R.string.push_token_cleared) :
-								getString(R.string.push_reset_text)) :
-						getString(R.string.gcm_register_failed)).show(getFragmentManager(), DIALOG_TAG_GCM_RESULT);
+				DialogUtil.dismissDialog(getParentFragmentManager(), DIALOG_TAG_PUSH_REGISTER, true);
+
+				String message;
+				if (intent.getBooleanExtra(PushUtil.EXTRA_REGISTRATION_ERROR_BROADCAST, false)) {
+					message = getString(R.string.token_register_failed);
+				}
+				else if (intent.getBooleanExtra(PushUtil.EXTRA_CLEAR_TOKEN, false)) {
+					message = getString(R.string.push_token_cleared);
+				}
+				else {
+					message = getString(R.string.push_reset_text);
+				}
+				SimpleStringAlertDialog.newInstance(-1, message).show(getParentFragmentManager(), DIALOG_TAG_PUSH_RESULT);
 			}
 		};
 
-		isPlayServicesInstalled = ConfigUtils.isPlayServicesInstalled(getContext());
-
 		pollingTwoStatePreference = (TwoStatePreference) findPreference(getResources().getString(R.string.preferences__polling_switch));
 		pollingTwoStatePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
 			public boolean onPreferenceChange(Preference preference, Object newValue) {
 				boolean newCheckedValue = newValue.equals(true);
 				if (((TwoStatePreference) preference).isChecked() != newCheckedValue) {
 					if (newCheckedValue) {
-						if (isPlayServicesInstalled) {
+						if (pushServicesInstalled) {
 							GenericAlertDialog dialog = GenericAlertDialog.newInstance(R.string.enable_polling, R.string.push_disable_text, R.string.continue_anyway, R.string.cancel);
 							dialog.setTargetFragment(SettingsTroubleshootingFragment.this, 0);
-							dialog.show(getFragmentManager(), DIALOG_TAG_REALLY_ENABLE_POLLING);
+							dialog.show(getParentFragmentManager(), DIALOG_TAG_REALLY_ENABLE_POLLING);
 							return false;
 						}
 						updatePollInterval();
 						return true;
 					} else {
-						if (isPlayServicesInstalled) {
+						if (pushServicesInstalled) {
 							lifetimeService.setPollingInterval(0);
 						} else {
 							Toast.makeText(getContext(), R.string.play_services_not_installed_unable_to_use_push, Toast.LENGTH_SHORT).show();
@@ -233,10 +237,10 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 		resetPushPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
 			@Override
 			public boolean onPreferenceClick(Preference preference) {
-				if (ConfigUtils.isPlayServicesInstalled(getActivity())) {
+				if (pushServicesInstalled) {
 					PushUtil.clearPushTokenSentDate(getActivity());
-					PushUtil.sendPushTokenToServer(getContext(), false, true);
-					GenericProgressDialog.newInstance(R.string.push_reset_title, R.string.please_wait).showNow(getFragmentManager(), DIALOG_TAG_GCM_REGISTER);
+					PushUtil.enqueuePushTokenUpdate(getContext(), false, true);
+					GenericProgressDialog.newInstance(R.string.push_reset_title, R.string.please_wait).showNow(getParentFragmentManager(), DIALOG_TAG_PUSH_REGISTER);
 				}
 				return true;
 			}
@@ -252,7 +256,7 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 						R.string.cancel);
 
 				dialog.setTargetFragment(SettingsTroubleshootingFragment.this, 0);
-				dialog.show(getFragmentManager(), DIALOG_TAG_REMOVE_WALLPAPERS);
+				dialog.show(getParentFragmentManager(), DIALOG_TAG_REMOVE_WALLPAPERS);
 				return false;
 			}
 		});
@@ -267,7 +271,7 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 						R.string.cancel);
 
 				dialog.setTargetFragment(SettingsTroubleshootingFragment.this, 0);
-				dialog.show(getFragmentManager(), DIALOG_TAG_RESET_RINGTONES);
+				dialog.show(getParentFragmentManager(), DIALOG_TAG_RESET_RINGTONES);
 				return false;
 			}
 		});
@@ -290,7 +294,7 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 
 						dialog.setTargetFragment(SettingsTroubleshootingFragment.this, 0);
 						dialog.setData(oldCheckedValue);
-						dialog.show(getFragmentManager(), DIALOG_TAG_IPV6_APP_RESTART);
+						dialog.show(getParentFragmentManager(), DIALOG_TAG_IPV6_APP_RESTART);
 						return false;
 					}
 					return true;
@@ -313,7 +317,7 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 								R.string.cancel);
 
 						dialog.setTargetFragment(SettingsTroubleshootingFragment.this, 0);
-						dialog.show(getFragmentManager(), DIALOG_TAG_POWERMANAGER_WORKAROUNDS);
+						dialog.show(getParentFragmentManager(), DIALOG_TAG_POWERMANAGER_WORKAROUNDS);
 					} else {
 						disableAutostart();
 					}
@@ -518,7 +522,7 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 				R.string.cancel);
 
 			dialog.setTargetFragment(SettingsTroubleshootingFragment.this, 0);
-			dialog.show(getFragmentManager(), DIALOG_TAG_AUTOSTART_WORKAROUNDS);
+			dialog.show(getParentFragmentManager(), DIALOG_TAG_AUTOSTART_WORKAROUNDS);
 		} else {
 			requestDisableBatteryOptimizations(getString(R.string.app_name), R.string.cancel, REQUEST_ID_DISABLE_BATTERY_OPTIMIZATIONS_HUAWEI);
 		}
@@ -529,16 +533,16 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 		super.onStart();
 
 		sharedPreferences.registerOnSharedPreferenceChangeListener(this);
-		LocalBroadcastManager.getInstance(getActivity()).registerReceiver(gcmRegisterBroadcastReceiver,
-				new IntentFilter(ThreemaApplication.INTENT_GCM_REGISTRATION_COMPLETE));
+		LocalBroadcastManager.getInstance(getActivity()).registerReceiver(pushTokenResetBroadcastReceiver,
+				new IntentFilter(ThreemaApplication.INTENT_PUSH_REGISTRATION_COMPLETE));
 	}
 
 	@Override
 	public void onStop() {
 		sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
-		LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(gcmRegisterBroadcastReceiver);
+		LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(pushTokenResetBroadcastReceiver);
 
-		DialogUtil.dismissDialog(getFragmentManager(), DIALOG_TAG_GCM_REGISTER, true);
+		DialogUtil.dismissDialog(getParentFragmentManager(), DIALOG_TAG_PUSH_REGISTER, true);
 
 		super.onStop();
 	}
@@ -555,13 +559,9 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 				return;
 			}
 
-			if (isPlayServicesInstalled) {
-				PushUtil.sendPushTokenToServer(getContext(), newValue, true);
-				GenericProgressDialog.newInstance(-1, R.string.please_wait).showNow(getFragmentManager(), DIALOG_TAG_GCM_REGISTER);
-			} else {
-				if (newValue) { // polling enabled
-					PushUtil.sendPushTokenToServer(getContext(), true, false);
-				}
+			if (pushServicesInstalled) {
+				PushUtil.enqueuePushTokenUpdate(getContext(), newValue, true);
+				GenericProgressDialog.newInstance(R.string.push_reset_title, R.string.please_wait).showNow(getParentFragmentManager(), DIALOG_TAG_PUSH_REGISTER);
 			}
 		} else if (key.equals(getString(R.string.preferences__polling_interval))) {
 			updatePollInterval();
@@ -577,7 +577,7 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 				3000,
 				1);
 		dialog.setTargetFragment(this, 0);
-		dialog.show(getFragmentManager(), DIALOG_TAG_SENDLOG);
+		dialog.show(getParentFragmentManager(), DIALOG_TAG_SENDLOG);
 	}
 
 
@@ -587,7 +587,7 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 
 			@Override
 			protected void onPreExecute() {
-				GenericProgressDialog.newInstance(R.string.preparing_messages, R.string.please_wait).show(getFragmentManager(), DIALOG_TAG_SENDLOG);
+				GenericProgressDialog.newInstance(R.string.preparing_messages, R.string.please_wait).show(getParentFragmentManager(), DIALOG_TAG_SENDLOG);
 			}
 
 			@Override
@@ -631,7 +631,7 @@ public class SettingsTroubleshootingFragment extends ThreemaPreferenceFragment i
 			@Override
 			protected void onPostExecute(Exception exception) {
 				if (isAdded()) {
-					DialogUtil.dismissDialog(getFragmentManager(), DIALOG_TAG_SENDLOG, true);
+					DialogUtil.dismissDialog(getParentFragmentManager(), DIALOG_TAG_SENDLOG, true);
 
 					if (exception != null) {
 						Toast.makeText(getActivity().getApplicationContext(), R.string.an_error_occurred, Toast.LENGTH_LONG).show();

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

@@ -194,6 +194,9 @@ public class MessageProcessor implements MessageProcessorInterface {
 					case ProtocolDefines.DELIVERYRECEIPT_MSGUSERDEC:
 						state = MessageState.USERDEC;
 						break;
+					case ProtocolDefines.DELIVERYRECEIPT_MSGCONSUMED:
+						state = MessageState.CONSUMED;
+						break;
 
 				}
 				if (state != null) {

+ 3 - 0
app/src/main/java/ch/threema/app/receivers/AcknowledgeActionBroadcastReceiver.java

@@ -73,6 +73,9 @@ public class AcknowledgeActionBroadcastReceiver extends ActionBroadcastReceiver
 				if (success != null) {
 					Toast.makeText(context, success ? R.string.message_acknowledged : R.string.an_error_occurred, Toast.LENGTH_LONG).show();
 				}
+
+				notificationService.cancel(messageReceiver);
+
 				pendingResult.finish();
 			}
 		}.execute();

+ 2 - 0
app/src/main/java/ch/threema/app/receivers/DeclineActionBroadcastReceiver.java

@@ -74,6 +74,8 @@ public class DeclineActionBroadcastReceiver extends ActionBroadcastReceiver {
 				if (success != null) {
 					Toast.makeText(context, success ?  R.string.message_declined : R.string.an_error_occurred, Toast.LENGTH_LONG).show();
 				}
+				notificationService.cancel(messageReceiver);
+
 				pendingResult.finish();
 			}
 		}.execute();

+ 3 - 0
app/src/main/java/ch/threema/app/receivers/MarkReadActionBroadcastReceiver.java

@@ -66,6 +66,9 @@ public class MarkReadActionBroadcastReceiver extends ActionBroadcastReceiver {
 				if (success) {
 					logger.debug("Conversation read: " + messageReceiver.getUniqueIdString());
 				}
+
+				notificationService.cancel(messageReceiver);
+
 				pendingResult.finish();
 			}
 		}.execute();

+ 2 - 3
app/src/main/java/ch/threema/app/receivers/ReSendMessagesBroadcastReceiver.java

@@ -30,15 +30,13 @@ import android.widget.Toast;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import androidx.core.app.NotificationManagerCompat;
-
 import java.util.ArrayList;
 
+import androidx.core.app.NotificationManagerCompat;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.messagereceiver.MessageReceiver;
 import ch.threema.app.utils.IntentDataUtil;
-import ch.threema.app.utils.LogUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.DistributionListMessageModel;
@@ -69,6 +67,7 @@ public class ReSendMessagesBroadcastReceiver extends ActionBroadcastReceiver {
 						MessageReceiver messageReceiver = getMessageReceiverFromMessageModel(failedMessage);
 						try {
 							messageService.resendMessage(failedMessage, messageReceiver, null);
+							notificationService.cancel(messageReceiver);
 						} catch (Exception e) {
 							RuntimeUtil.runOnUiThread(new Runnable() {
 								@Override

+ 2 - 0
app/src/main/java/ch/threema/app/receivers/ReplyActionBroadcastReceiver.java

@@ -93,6 +93,8 @@ public class ReplyActionBroadcastReceiver extends ActionBroadcastReceiver {
 				if (success != null) {
 					Toast.makeText(context, success ? R.string.message_sent : R.string.verify_failed, Toast.LENGTH_LONG).show();
 				}
+				notificationService.cancel(messageReceiver);
+
 				pendingResult.finish();
 			}
 		}.execute();

+ 6 - 1
app/src/main/java/ch/threema/app/receivers/UpdateReceiver.java

@@ -24,10 +24,13 @@ package ch.threema.app.receivers;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.SharedPreferences;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import androidx.preference.PreferenceManager;
+import ch.threema.app.ThreemaApplication;
 import ch.threema.app.utils.PushUtil;
 
 public class UpdateReceiver extends BroadcastReceiver {
@@ -38,8 +41,10 @@ public class UpdateReceiver extends BroadcastReceiver {
 		if (intent != null && intent.getAction() != null && intent.getAction().equals(Intent.ACTION_MY_PACKAGE_REPLACED)) {
 			logger.info("*** App was updated ***");
 
-			// force GCM register
+			// force token register
 			PushUtil.clearPushTokenSentDate(context);
 		}
 	}
+
+
 }

+ 37 - 4
app/src/main/java/ch/threema/app/routines/CheckLicenseRoutine.java

@@ -21,9 +21,12 @@
 
 package ch.threema.app.routines;
 
+import android.app.Activity;
 import android.content.Context;
 import android.content.ReceiverCallNotAllowedException;
 
+import com.DrmSDK.Drm;
+import com.DrmSDK.DrmCheckCallback;
 import com.google.android.vending.licensing.LicenseChecker;
 import com.google.android.vending.licensing.LicenseCheckerCallback;
 
@@ -56,6 +59,9 @@ public class CheckLicenseRoutine implements Runnable {
 	private final IdentityStoreInterface identityStore;
 	private static final String LICENSE_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqJArbOQT3Vi2KUEbyk+xq+DSsowwIYoudh3miXC7DmR6SVL6ji7XG8C+hmtR6t+Ytar64z87xgTPiEPiuyyg6/fp8ALRLAjM2FmZadSS4hSpvmJKb2ViFyUmcCJ8MoZ2QPxA+SVGZFdwIwwXdHPx2xUQw6ftyx0EF0hvF4nwHLvq89p03QtiPnIb0A3MOEXsq88xu2xAUge/BTvRWo0gWTtIJhTdZXY2CSib5d/G45xca0DKgOECAaMxVbFhE5jSyS+qZvUN4tABgDKBiEPuuzBBaHVt/m7MQoqoM6kcNrozACmIx6UdwWbkK3Isa9Xo9g3Yy6oc9Mp/9iKXwco4vwIDAQAB";
 
+	private static final String HMS_ID = "5190041000024384032";
+	private static final String HMS_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA26ccdC7mLHomHTnKvSRGg7Vuex19xD3qv8CEOUj5lcT5Z81ARby5CVhM/ZM9zKCQcrKmenn1aih6X+uZoNsvBziDUySkrzXPTX/NfoFDQlHgyXan/xsoIPlE1v0D9dLV7fgPOllHxmN8wiwF+woACo3ao/ra2VY38PCZTmfMX/V+hOLHsdRakgWVshzeYTtzMjlLrnYOp5AFXEjFhF0dB92ozAmLzjFJtwyMdpbVD+yRVr+fnLJ6ADhBpoKLjvpn8A7PhpT5wsvogovdr16u/uKhPy5an4DXE0bjWc76bE2SEse/bQTvPoGRw5TjHVWi7uDMFSz3OOGUqLSygucPdwIDAQAB";
+
 	public CheckLicenseRoutine(Context context,
 	                           APIConnector apiConnector,
 	                           UserService userService,
@@ -87,8 +93,12 @@ public class CheckLicenseRoutine implements Runnable {
 					break;
 				case SERIAL:
 				case GOOGLE_WORK:
+				case HMS_WORK:
 					this.checkSerial();
 					break;
+				case HMS:
+					this.checkDRM();
+					break;
 			}
 		}
 	}
@@ -100,7 +110,6 @@ public class CheckLicenseRoutine implements Runnable {
 		if(error != null) {
 			invalidLicense(error);
 		} else {
-
 			userService.setCredentials(licenseService.loadCredentials());
 
 			if(licenseService instanceof LicenseServiceThreema) {
@@ -133,14 +142,38 @@ public class CheckLicenseRoutine implements Runnable {
 		}
 	}
 
+	private void checkDRM() {
+		logger.debug("Check HMS license");
+
+		if (this.deviceService.isOnline() && !userService.hasIdentity()) {
+			DrmCheckCallback callback = new DrmCheckCallback() {
+				@Override
+				public void onCheckSuccess(String signData, String signature) {
+					logger.info("HMS License OK");
+					userService.setPolicyResponse(
+						signData,
+						signature
+					);
+				}
+
+				@Override
+				public void onCheckFailed(int errorCode) {
+					logger.debug("HMS License failed errorCode: {}", errorCode);
+
+				}
+			};
+			Drm.check((Activity) context, context.getPackageName(), HMS_ID, HMS_PUBLIC_KEY, callback);
+		}
+	}
+
 	private void checkLVL() {
-		logger.debug("check lvl");
+		logger.debug("Check GCM licence");
 		if(this.deviceService.isOnline()) {
 			final ThreemaLicensePolicy policy = new ThreemaLicensePolicy();
 			LicenseCheckerCallback callback = new LicenseCheckerCallback() {
 				@Override
 				public void allow(int reason) {
-					logger.debug("License OK");
+					logger.debug("GCM License OK");
 					userService.setPolicyResponse(
 							policy.getLastResponseData().responseData,
 							policy.getLastResponseData().signature
@@ -157,7 +190,7 @@ public class CheckLicenseRoutine implements Runnable {
 
 				@Override
 				public void applicationError(int errorCode) {
-					logger.debug("License check failed (code " + errorCode + ")");
+					logger.debug("GCM License check failed errorCode: {}", errorCode);
 					//invalidLicense("License check failed (code " + errorCode + ")");
 				}
 			};

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

@@ -400,7 +400,7 @@ final public class AvatarCacheServiceImpl implements AvatarCacheService {
 						groupImage = AvatarConverterUtil.getAvatarBitmap(groupDefaultAvatar, color, this.avatarSizeSmall);
 					}
 				}
-			} else {
+			} else if (!highResolution) {
 				//resize image!
 				Bitmap converted = AvatarConverterUtil.convert(this.context.getResources(), groupImage);
 				if (groupImage != converted) {

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

@@ -67,6 +67,7 @@ import ch.threema.app.collections.Functional;
 import ch.threema.app.collections.IPredicateNonNull;
 import ch.threema.app.exceptions.EntryAlreadyExistsException;
 import ch.threema.app.exceptions.InvalidEntryException;
+import ch.threema.app.exceptions.PolicyViolationException;
 import ch.threema.app.listeners.ContactListener;
 import ch.threema.app.listeners.ContactSettingsListener;
 import ch.threema.app.listeners.ContactTypingListener;
@@ -85,9 +86,7 @@ import ch.threema.app.utils.BitmapUtil;
 import ch.threema.app.utils.ColorUtil;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.ContactUtil;
-import ch.threema.app.utils.NameUtil;
 import ch.threema.app.utils.TestUtil;
-import ch.threema.app.exceptions.PolicyViolationException;
 import ch.threema.base.ThreemaException;
 import ch.threema.base.VerificationLevel;
 import ch.threema.client.APIConnector;
@@ -1005,7 +1004,9 @@ public class ContactServiceImpl implements ContactService {
 
 		if (contact == null) return;
 
-		if(contact.getPublicNickName() == null || !contact.getPublicNickName().equals(msg.getPushFromName())) {
+		if(msg.getPushFromName() != null && msg.getPushFromName().length() > 0 &&
+			!msg.getPushFromName().equals(contact.getIdentity()) &&
+			!msg.getPushFromName().equals(contact.getPublicNickName())) {
 			contact.setPublicNickName(msg.getPushFromName());
 			this.save(contact);
 		}
@@ -1076,7 +1077,6 @@ public class ContactServiceImpl implements ContactService {
 								&& ConfigUtils.isCallsEnabled(context, preferenceService, licenseService);
 							lookupKey = AndroidContactUtil.getInstance().createThreemaAndroidContact(
 								contactModel.getIdentity(),
-								NameUtil.getDisplayName(contactModel),
 								supportsVoiceCalls);
 
 							logger.debug("created android contact, lookup key " + lookupKey);

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

@@ -24,6 +24,7 @@ package ch.threema.app.services;
 import java.util.List;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import ch.threema.storage.models.ConversationModel;
 import ch.threema.storage.models.ConversationTagModel;
 import ch.threema.storage.models.TagModel;
@@ -37,42 +38,42 @@ public interface ConversationTagService {
 	/**
 	 * Select a {@link TagModel} by the key
 	 */
-	TagModel getTagModel(String tagKey);
+	@Nullable TagModel getTagModel(@NonNull String tagKey);
 
 	/**
 	 * Return all tags for the specified  {@link ConversationModel}.
 	 */
-	List<ConversationTagModel> getTagsForConversation(final ConversationModel conversation);
+	List<ConversationTagModel> getTagsForConversation(@NonNull final ConversationModel conversation);
 
 	/**
 	 * Tag the {@link ConversationModel} with the given {@link TagModel}
 	 */
-	boolean tag(ConversationModel conversation, TagModel tagModel);
+	boolean tag(@Nullable ConversationModel conversation, @Nullable TagModel tagModel);
 
 	/**
 	 * Untag the {@link ConversationModel} with the given {@link TagModel}
 	 */
-	boolean unTag(ConversationModel conversation, TagModel tagModel);
+	boolean unTag(@Nullable ConversationModel conversation, @Nullable TagModel tagModel);
 
 	/**
 	 * Toggle the {@link TagModel} of the {@link ConversationModel}
 	 */
-	boolean toggle(ConversationModel ConversationModel, TagModel tagModel, boolean silent);
+	boolean toggle(@Nullable ConversationModel ConversationModel, @Nullable TagModel tagModel, boolean silent);
 
 	/**
 	 * Return true, if the {@link ConversationModel} is tagged with {@link TagModel}
 	 */
-	boolean isTaggedWith(ConversationModel ConversationModel, TagModel tagModel);
+	boolean isTaggedWith(@Nullable ConversationModel ConversationModel, @Nullable TagModel tagModel);
 
 	/**
 	 * Remove all tags linked with the given {@link ConversationModel}
 	 */
-	void removeAll(ConversationModel conversation);
+	void removeAll(@Nullable ConversationModel conversation);
 
 	/**
 	 * Remove all tags linked with the given {@link TagModel}
 	 */
-	void removeAll(TagModel tagModel);
+	void removeAll(@Nullable TagModel tagModel);
 
 	/**
 	 * Get all tags regardless of type

+ 24 - 33
app/src/main/java/ch/threema/app/services/ConversationTagServiceImpl.java

@@ -21,18 +21,14 @@
 
 package ch.threema.app.services;
 
-import android.content.Context;
-
 import java.util.ArrayList;
 import java.util.List;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import ch.threema.app.R;
-import ch.threema.app.collections.Functional;
-import ch.threema.app.collections.IPredicateNonNull;
 import ch.threema.app.listeners.ConversationListener;
 import ch.threema.app.managers.ListenerManager;
-import ch.threema.app.utils.TestUtil;
 import ch.threema.storage.DatabaseServiceNew;
 import ch.threema.storage.models.ConversationModel;
 import ch.threema.storage.models.ConversationTagModel;
@@ -44,27 +40,21 @@ public class ConversationTagServiceImpl implements ConversationTagService {
 	public static final String FIXED_TAG_UNREAD = "unread"; // chats deliberately marked as unread
 
 	private final DatabaseServiceNew databaseService;
-
-	private final List<TagModel> tagModels = new ArrayList<>();
-
-	public ConversationTagServiceImpl(Context context, DatabaseServiceNew databaseService) {
-		this.databaseService = databaseService;
-
-		// Initalize Tag Models
-		this.tagModels.add(new TagModel(
+	private final List<TagModel> tagModels = new ArrayList<TagModel>() {{
+		add(new TagModel(
 			FIXED_TAG_PIN,
 			1,
 			2,
-			context.getString(R.string.pin)
-		));
-
-		// Initalize Tag Models
-		this.tagModels.add(new TagModel(
+			R.string.pin));
+		add(new TagModel(
 			FIXED_TAG_UNREAD,
 			0xFFFF0000,
 			0xFFFFFFFF,
-			context.getString(R.string.unread_messages)
-		));
+			R.string.unread));
+	}};
+
+	public ConversationTagServiceImpl(DatabaseServiceNew databaseService) {
+		this.databaseService = databaseService;
 	}
 
 	@Override
@@ -73,23 +63,24 @@ public class ConversationTagServiceImpl implements ConversationTagService {
 	}
 
 	@Override
-	public TagModel getTagModel(final String tagKey) {
-		return Functional.select(this.tagModels, new IPredicateNonNull<TagModel>() {
-			@Override
-			public boolean apply(@NonNull TagModel tagModel) {
-				return TestUtil.compare(tagModel.getTag(), tagKey);
+	@Nullable
+	public TagModel getTagModel(@NonNull final String tagKey) {
+		for (TagModel tagModel : this.tagModels) {
+			if (tagKey.equals(tagModel.getTag())) {
+				return tagModel;
 			}
-		});
+		}
+		return null;
 	}
 
 	@Override
-	public List<ConversationTagModel> getTagsForConversation(final ConversationModel conversation) {
+	public List<ConversationTagModel> getTagsForConversation(@NonNull final ConversationModel conversation) {
 		return this.databaseService.getConversationTagFactory()
 			.getByConversationUid(conversation.getUid());
 	}
 
 	@Override
-	public boolean tag(ConversationModel conversation, TagModel tagModel) {
+	public boolean tag(@Nullable ConversationModel conversation, @Nullable TagModel tagModel) {
 		if (conversation != null && tagModel != null) {
 			if (!this.isTaggedWith(conversation, tagModel)) {
 				this.databaseService.getConversationTagFactory()
@@ -102,7 +93,7 @@ public class ConversationTagServiceImpl implements ConversationTagService {
 	}
 
 	@Override
-	public boolean unTag(ConversationModel conversation, TagModel tagModel) {
+	public boolean unTag(@Nullable ConversationModel conversation, @Nullable TagModel tagModel) {
 		if (conversation != null && tagModel != null) {
 			if (this.isTaggedWith(conversation, tagModel)) {
 				this.databaseService.getConversationTagFactory()
@@ -115,7 +106,7 @@ public class ConversationTagServiceImpl implements ConversationTagService {
 	}
 
 	@Override
-	public boolean toggle(ConversationModel conversation, TagModel tagModel, boolean silent) {
+	public boolean toggle(@Nullable ConversationModel conversation, @Nullable TagModel tagModel, boolean silent) {
 		if (conversation != null && tagModel != null) {
 			if (this.isTaggedWith(conversation, tagModel)) {
 				// remove
@@ -137,7 +128,7 @@ public class ConversationTagServiceImpl implements ConversationTagService {
 	}
 
 	@Override
-	public boolean isTaggedWith(ConversationModel conversation, TagModel tagModel) {
+	public boolean isTaggedWith(@Nullable ConversationModel conversation, @Nullable TagModel tagModel) {
 		if (conversation == null || tagModel == null) {
 			return false;
 		}
@@ -147,7 +138,7 @@ public class ConversationTagServiceImpl implements ConversationTagService {
 	}
 
 	@Override
-	public void removeAll(ConversationModel conversation) {
+	public void removeAll(@Nullable ConversationModel conversation) {
 		if (conversation != null) {
 			this.databaseService.getConversationTagFactory()
 				.deleteByConversationUid(conversation.getUid());
@@ -155,7 +146,7 @@ public class ConversationTagServiceImpl implements ConversationTagService {
 	}
 
 	@Override
-	public void removeAll(TagModel tagModel) {
+	public void removeAll(@Nullable TagModel tagModel) {
 		if (tagModel != null) {
 			this.databaseService.getConversationTagFactory()
 				.deleteByConversationTag(tagModel.getTag());

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

@@ -55,7 +55,8 @@ public interface FileService {
 	File getBackupPath();
 
 	/**
-	 * get the Uri for data backup files
+	 * Get the Uri for data backup files
+	 * @return Uri of data backup path or null if not yet selected by user
 	 */
 	Uri getBackupUri();
 

+ 9 - 3
app/src/main/java/ch/threema/app/services/FileServiceImpl.java

@@ -210,19 +210,25 @@ public class FileServiceImpl implements FileService {
 
 	@Deprecated
 	public File getBackupPath() {
-		if (!this.backupPath.exists()) {
-			this.backupPath.mkdirs();
+		if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+			if (!this.backupPath.exists()) {
+				this.backupPath.mkdirs();
+			}
 		}
 		return this.backupPath;
 	}
 
 	@Override
-	public @NonNull Uri getBackupUri() {
+	public @Nullable Uri getBackupUri() {
 		// check if backup path is overridden by user
 		Uri backupUri = preferenceService.getDataBackupUri();
 		if (backupUri != null) {
 			return backupUri;
 		}
+
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+			return null;
+		}
 		return Uri.fromFile(getBackupPath());
 	}
 

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

@@ -35,7 +35,6 @@ import androidx.annotation.AnyThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
-import ch.threema.app.activities.RecipientListBaseActivity;
 import ch.threema.app.messagereceiver.MessageReceiver;
 import ch.threema.app.ui.MediaItem;
 import ch.threema.base.ThreemaException;
@@ -143,6 +142,10 @@ public interface MessageService {
 	void updateMessageState(final MessageId apiMessageId, final String identity, MessageState state, Date stateDate);
 	void updateMessageStateAtOutboxed(final MessageId apiMessageId, MessageState state, Date stateDate);
 	boolean markAsRead(AbstractMessageModel message, boolean silent) throws ThreemaException;
+
+	@WorkerThread
+	boolean markAsConsumed(AbstractMessageModel message) throws ThreemaException;
+
 	void remove(AbstractMessageModel messageModel);
 
 	/**

+ 67 - 13
app/src/main/java/ch/threema/app/services/MessageServiceImpl.java

@@ -38,7 +38,6 @@ import android.text.format.DateUtils;
 import android.util.SparseIntArray;
 import android.widget.Toast;
 
-import com.google.android.gms.common.util.ArrayUtils;
 import com.neilalexander.jnacl.NaCl;
 
 import org.apache.commons.io.IOUtils;
@@ -79,6 +78,7 @@ import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 import androidx.collection.ArrayMap;
 import androidx.core.app.NotificationManagerCompat;
+import ch.threema.app.BuildConfig;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.collections.Functional;
@@ -475,16 +475,26 @@ public class MessageServiceImpl implements MessageService {
 		return messageModel;
 	}
 
-	private Set<ContactModel> addProfilePicRecipient(Set<ContactModel> contacts, ContactModel contact, UserService userService, Date lastUpdated) {
-		if (contact != null) {
+	/**
+	 * Add provided contact to a list of contacts
+	 * - if it never received our profile picture
+	 * - if our profile pic was updated since it last received it
+	 * @param contacts List of contacts to add "contact" to if it should receive the profile picture
+	 * @param contact ContactModel to examine
+	 * @param userService
+	 * @param lastUpdated Date our profile pic was last updated
+	 * @return updated contacts
+	 */
+	private Set<ContactModel> addProfilePicRecipient(Set<ContactModel> contacts, ContactModel contact, UserService userService, @Nullable Date lastUpdated) {
+		if (contact != null && lastUpdated != null) {
 			String identity = contact.getIdentity();
 			if (!userService.getIdentity().equals(identity)) {
 				if (preferenceService.getProfilePicRelease() == PreferenceService.PROFILEPIC_RELEASE_EVERYONE ||
 						(preferenceService.getProfilePicRelease() == PreferenceService.PROFILEPIC_RELEASE_SOME &&
 						profilePicRecipientsService.has(identity))) {
-					Date date = contact.getProfilePicSentDate();
+					Date profilePicSentDate = contact.getProfilePicSentDate();
 
-					if (date == null || lastUpdated.after(date)) {
+					if (profilePicSentDate == null || lastUpdated.after(profilePicSentDate)) {
 						contacts.add(contact);
 					}
 				}
@@ -496,11 +506,6 @@ public class MessageServiceImpl implements MessageService {
 	@Override
 	public boolean sendProfilePicture(MessageReceiver[] messageReceivers) {
 		if (messageReceivers.length > 0) {
-			Date lastUpdated = preferenceService.getProfilePicLastUpdate();
-			if (lastUpdated == null) {
-				return false;
-			}
-
 			UserService userService;
 			try {
 				userService = ThreemaApplication.getServiceManager().getUserService();
@@ -511,6 +516,8 @@ public class MessageServiceImpl implements MessageService {
 				return false;
 			}
 
+			Date lastUpdated = preferenceService.getProfilePicLastUpdate();
+
 			// create array of receivers that need an update
 			Set<ContactModel> outdatedContacts = new HashSet<>();
 			Set<ContactModel> restoredContacts = new HashSet<>();
@@ -1070,7 +1077,7 @@ public class MessageServiceImpl implements MessageService {
 
 	@Override
 	public boolean markAsRead(AbstractMessageModel message, boolean silent) throws ThreemaException {
-		logger.debug("markAsRead message = " + message.getApiMessageId() + " silent = " + silent);
+		logger.debug("markAsRead message = {} silent = {}", message.getApiMessageId(), silent);
 		boolean saved = false;
 
 		if (MessageUtil.canMarkAsRead(message)) {
@@ -1106,6 +1113,43 @@ public class MessageServiceImpl implements MessageService {
 		return saved;
 	}
 
+	@Override
+	@WorkerThread
+	public boolean markAsConsumed(AbstractMessageModel message) throws ThreemaException {
+		logger.debug("markAsConsumed message = {}", message.getApiMessageId());
+		boolean saved = false;
+
+		if (MessageUtil.canMarkAsConsumed(message)) {
+			// save consumed state
+			message.setState(MessageState.CONSUMED);
+			message.setModifiedAt(new Date());
+
+			this.save(message);
+
+			saved = true;
+
+			if (BuildConfig.SEND_CONSUMED_DELIVERY_RECEIPTS) {
+				if (this.preferenceService.isReadReceipts()
+					&& message instanceof MessageModel
+					&& !((message.getMessageFlags() & ProtocolDefines.MESSAGE_FLAG_NO_DELIVERY_RECEIPTS) == ProtocolDefines.MESSAGE_FLAG_NO_DELIVERY_RECEIPTS)) {
+					DeliveryReceiptMessage receipt = new DeliveryReceiptMessage();
+					receipt.setReceiptType(ProtocolDefines.DELIVERYRECEIPT_MSGCONSUMED);
+
+					receipt.setReceiptMessageIds(new MessageId[]{new MessageId(Utils.hexStringToByteArray(message.getApiMessageId()))});
+					receipt.setFromIdentity(this.identityStore.getIdentity());
+					receipt.setToIdentity(message.getIdentity());
+					logger.info("Enqueue delivery receipt (consumed) message ID {} for message ID {} from {}",
+						receipt.getMessageId(), receipt.getReceiptMessageIds()[0], receipt.getToIdentity());
+					this.messageQueue.enqueue(receipt);
+				}
+			}
+
+			fireOnModifiedMessage(message);
+		}
+
+		return saved;
+	}
+
 	@Override
 	public void remove(AbstractMessageModel messageModel) {
 		this.remove(messageModel, false);
@@ -3641,7 +3685,12 @@ public class MessageServiceImpl implements MessageService {
 							}
 						}
 						if (imageByteArray != null) {
-							return ArrayUtils.concatByteArrays(new byte[NaCl.BOXOVERHEAD], imageByteArray);
+							fileDataModel.setFileSize(imageByteArray.length);
+							ByteArrayOutputStream outputStream = new ByteArrayOutputStream( );
+							outputStream.write( new byte[NaCl.BOXOVERHEAD] );
+							outputStream.write( imageByteArray );
+
+							return outputStream.toByteArray( );
 						}
 					}
 				} catch (Exception e) {
@@ -3662,7 +3711,12 @@ public class MessageServiceImpl implements MessageService {
 								bitmap,
 								mediaItem.getExifRotation(),
 								mediaItem.getExifFlip()), mediaItem.getRotation(), mediaItem.getFlip());
-							return ArrayUtils.concatByteArrays(new byte[NaCl.BOXOVERHEAD], BitmapUtil.getJpegByteArray(bitmap, mediaItem.getRotation(), mediaItem.getFlip()));
+
+							ByteArrayOutputStream outputStream = new ByteArrayOutputStream( );
+							outputStream.write( new byte[NaCl.BOXOVERHEAD] );
+							outputStream.write( BitmapUtil.getJpegByteArray(bitmap, mediaItem.getRotation(), mediaItem.getFlip()) );
+
+							return outputStream.toByteArray( );
 						}
 					}
 				} catch (Exception e) {

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

@@ -187,9 +187,7 @@ public class NotificationServiceImpl implements NotificationService {
 
 	public NotificationServiceImpl(Context context,
 	                               LockAppService lockAppService,
-	                               DeadlineListService mutedChatsListService,
 	                               DeadlineListService hiddenChatsListService,
-	                               DeadlineListService mentionOnlyChatsListService,
 	                               PreferenceService preferenceService,
 	                               RingtoneService ringtoneService) {
 		this.context = context;
@@ -787,7 +785,9 @@ public class NotificationServiceImpl implements NotificationService {
 					.setShowsUserInterface(false).build());
 			}
 
-			if (newestGroup.getMessageReceiver() instanceof ContactMessageReceiver) {
+			if (newestGroup.getMessageReceiver() instanceof GroupMessageReceiver) {
+				builder.addAction(getMarkAsReadAction(markReadPendingIntent));
+			} else if (newestGroup.getMessageReceiver() instanceof ContactMessageReceiver) {
 
 				if (conversationNotification.getMessageType().equals(MessageType.VOIP_STATUS))  {
 					// Create an intent for the call action
@@ -812,13 +812,17 @@ public class NotificationServiceImpl implements NotificationService {
 						builder.addAction(new NotificationCompat.Action.Builder(R.drawable.ic_thumb_up_white_24dp, context.getString(R.string.acknowledge), ackPendingIntent)
 								.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_THUMBS_UP).build());
 					}
-					builder.addAction(new NotificationCompat.Action.Builder(R.drawable.ic_mark_read, context.getString(R.string.mark_read_short), markReadPendingIntent)
-						.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ).build());
+					builder.addAction(getMarkAsReadAction(markReadPendingIntent));
 				}
 			}
 		}
 	}
 
+	private NotificationCompat.Action getMarkAsReadAction(PendingIntent markReadPendingIntent) {
+		return new NotificationCompat.Action.Builder(R.drawable.ic_mark_read, context.getString(R.string.mark_read_short), markReadPendingIntent)
+			.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ).build();
+	}
+
 	private void createSingleNotification(ConversationNotificationGroup newestGroup,
 										 ConversationNotification newestNotification,
 										 PendingIntent openPendingIntent,
@@ -1037,7 +1041,7 @@ public class NotificationServiceImpl implements NotificationService {
 			synchronized (this.conversationNotifications) {
 				cancelPinLockedNewMessagesNotification();
 
-				//check if more then one group in the notification
+				//check if more than one group in the notification
 				ConversationNotification newestNotification = null;
 				HashSet<ConversationNotificationGroup> uniqueNotificationGroups = new HashSet<>();
 
@@ -1130,7 +1134,7 @@ public class NotificationServiceImpl implements NotificationService {
 		synchronized (this.conversationNotifications) {
 			cancelPinLockedNewMessagesNotification();
 
-			//check if more then one group in the notification
+			//check if more than one group in the notification
 			ConversationNotificationGroup newestGroup = null;
 			ConversationNotification newestNotification = null;
 			Map<String, ConversationNotificationGroup> uniqueNotificationGroups = new HashMap<>();

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

@@ -930,7 +930,7 @@ public class PreferenceServiceImpl implements PreferenceService {
 	@Override
 	public void setPushToken(String gcmToken) {
 		this.preferenceStore.save(
-			this.getKeyName(R.string.preferences__gcm_token),
+			this.getKeyName(R.string.preferences__push_token),
 			gcmToken,
 			true);
 	}
@@ -938,7 +938,7 @@ public class PreferenceServiceImpl implements PreferenceService {
 	@Override
 	public String getPushToken() {
 		return this.preferenceStore.getString(
-			this.getKeyName(R.string.preferences__gcm_token),
+			this.getKeyName(R.string.preferences__push_token),
 			true
 		);
 	}

+ 66 - 23
app/src/main/java/ch/threema/app/services/UserServiceImpl.java

@@ -28,6 +28,8 @@ import android.content.ContentResolver;
 import android.content.Context;
 import android.provider.ContactsContract;
 
+import org.json.JSONException;
+import org.json.JSONObject;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -39,6 +41,7 @@ import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import ch.threema.app.BuildFlavor;
 import ch.threema.app.R;
+import ch.threema.app.ThreemaApplication;
 import ch.threema.app.collections.Functional;
 import ch.threema.app.collections.IPredicateNonNull;
 import ch.threema.app.listeners.SMSVerificationListener;
@@ -58,6 +61,7 @@ import ch.threema.app.utils.PushUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.base.ThreemaException;
 import ch.threema.client.APIConnector;
+import ch.threema.client.CreateIdentityRequestDataInterface;
 import ch.threema.client.IdentityBackupDecoder;
 import ch.threema.client.IdentityStoreInterface;
 import ch.threema.client.MessageQueue;
@@ -71,7 +75,7 @@ import static ch.threema.app.ThreemaApplication.PHONE_LINKED_PLACEHOLDER;
 /**
  * This service class handle all user actions (db/identity....)
  */
-public class UserServiceImpl implements UserService {
+public class UserServiceImpl implements UserService, CreateIdentityRequestDataInterface  {
 	private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
 
 	private final Context context;
@@ -109,34 +113,23 @@ public class UserServiceImpl implements UserService {
 		if (this.hasIdentity()) {
 			throw new ThreemaException("please remove your existing identity " + this.getIdentity());
 		}
-		String licenseKey = null;
-		String licenseUsername = null;
-		String licensePassword = null;
 
-		if(this.credentials != null) {
-			if(this.credentials instanceof SerialCredentials) {
-				licenseKey = ((SerialCredentials)this.credentials).licenseKey;
-			}
-			else if(this.credentials instanceof UserCredentials) {
-				licenseUsername = ((UserCredentials)this.credentials).username;
-				licensePassword = ((UserCredentials)this.credentials).password;
-			}
+		// no need to send a request if we have no licence
+		if (policySignature == null && policyResponseData == null && credentials == null) {
+			throw new ThreemaException(ThreemaApplication.getAppContext().getResources().getString(R.string.missing_app_licence));    /* Create identity phase 1 unsuccessful:*/
 		}
-		this.apiConnector.createIdentity(
+		else {
+			this.apiConnector.createIdentity(
 				this.identityStore,
 				newRandomSeed,
-				this.policyResponseData,
-				this.policySignature,
-				DeviceIdUtil.getDeviceId(this.context),
-				licenseKey,
-				licenseUsername,
-				licensePassword
-		);
+				this
+			);
+		}
 
 		this.sendFlags();
 
 		// identity has been successfully created. set push token
-		PushUtil.scheduleSendPushTokenToServer(context);
+		PushUtil.enqueuePushTokenUpdate(context, false, false);
 	}
 
 	@Override
@@ -325,7 +318,8 @@ public class UserServiceImpl implements UserService {
 				normalizedMobileNo,
 				this.getLanguage(),
 				this.identityStore,
-				BuildFlavor.getLicenseType() == BuildFlavor.LicenseType.GOOGLE_WORK
+				(BuildFlavor.getLicenseType() == BuildFlavor.LicenseType.GOOGLE_WORK ||
+					BuildFlavor.getLicenseType() == BuildFlavor.LicenseType.HMS_WORK)
 					? "threemawork" : null
 		);
 
@@ -545,7 +539,7 @@ public class UserServiceImpl implements UserService {
 		}
 
 		// identity has been successfully restored. set push token
-		PushUtil.scheduleSendPushTokenToServer(context);
+		PushUtil.enqueuePushTokenUpdate(context, false, false);
 
 		return true;
 	}
@@ -646,4 +640,53 @@ public class UserServiceImpl implements UserService {
 			logger.error("Exception", e);
 		}
 	}
+
+	@Override
+	public JSONObject createIdentityRequestDataJSON() throws JSONException {
+		JSONObject baseObject = new JSONObject();
+
+		BuildFlavor.LicenseType licenseType = BuildFlavor.getLicenseType();
+		String deviceId = DeviceIdUtil.getDeviceId(this.context);
+
+		if (deviceId != null) {
+			baseObject.put("deviceId", deviceId);
+		}
+
+		if (licenseType == BuildFlavor.LicenseType.GOOGLE) {
+			baseObject.put("lvlResponseData", policyResponseData);
+			baseObject.put("lvlSignature", policySignature);
+		}
+		else if (licenseType == BuildFlavor.LicenseType.HMS) {
+			baseObject.put("hmsResponseData", policyResponseData);
+			baseObject.put("hmsSignature", policySignature);
+		}
+		else {
+			String licenseKey = null;
+			String licenseUsername = null;
+			String licensePassword = null;
+
+			if(this.credentials != null) {
+				if(this.credentials instanceof SerialCredentials) {
+					licenseKey = ((SerialCredentials)this.credentials).licenseKey;
+				}
+				else if(this.credentials instanceof UserCredentials) {
+					licenseUsername = ((UserCredentials)this.credentials).username;
+					licensePassword = ((UserCredentials)this.credentials).password;
+				}
+			}
+			if (licenseKey != null) {
+				baseObject.put("licenseKey", licenseKey);
+			}
+
+			if (licenseUsername != null) {
+				baseObject.put("licenseUsername", licenseUsername);
+			}
+
+			if (licensePassword != null) {
+				baseObject.put("licensePassword", licensePassword);
+			}
+		}
+
+		return baseObject;
+	}
 }

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

@@ -166,6 +166,7 @@ public class AudioMessagePlayer extends MessagePlayer implements AudioManager.On
 			mediaPlayer.setDataSource(getContext(), uri);
 			mediaPlayer.prepare();
 			prepared(mediaPlayer, resume);
+			markAsConsumed();
 		} catch (Exception e) {
 			if (e instanceof IllegalArgumentException) {
 				showError(getContext().getString(R.string.file_is_not_audio));

+ 11 - 0
app/src/main/java/ch/threema/app/services/messageplayer/MessagePlayer.java

@@ -38,6 +38,7 @@ import java.util.concurrent.RejectedExecutionException;
 import androidx.annotation.AnyThread;
 import androidx.annotation.MainThread;
 import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
 import ch.threema.app.R;
 import ch.threema.app.messagereceiver.MessageReceiver;
 import ch.threema.app.services.FileService;
@@ -45,6 +46,7 @@ import ch.threema.app.services.MessageService;
 import ch.threema.app.utils.FileUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.TestUtil;
+import ch.threema.base.ThreemaException;
 import ch.threema.client.ProgressListener;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.data.media.MediaMessageDataInterface;
@@ -657,4 +659,13 @@ public abstract class MessagePlayer {
 			this.exception(this.getContext().getString(error), x);
 		}
 	}
+
+	@WorkerThread
+	protected void markAsConsumed() {
+		try {
+			messageService.markAsConsumed(getMessageModel());
+		} catch (ThreemaException e) {
+			logger.error("Unable to mark message as consumed", e);
+		}
+	}
 }

+ 6 - 0
app/src/main/java/ch/threema/app/services/messageplayer/WebClientMessagePlayer.java

@@ -84,6 +84,12 @@ public class WebClientMessagePlayer extends MessagePlayer {
 		return null;
 	}
 
+	@Override
+	public boolean open() {
+		markAsConsumed();
+		return super.open();
+	}
+
 	@Override
 	protected void open(File decryptedFile) { }
 

+ 79 - 0
app/src/main/java/ch/threema/app/ui/FastScrollGridView.java

@@ -0,0 +1,79 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2013-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.ui;
+
+import android.content.Context;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.widget.AbsListView;
+import android.widget.GridView;
+
+import androidx.appcompat.view.ContextThemeWrapper;
+import ch.threema.app.R;
+import ch.threema.app.utils.RuntimeUtil;
+
+/**
+ * This class fixes two bugs in the Android framework
+ * - android:fastScrollStyle attribute ignored for GridViews
+ * - android:fastScrollAlwaysVisible="false" never ever showing fastscroll
+ */
+
+public class FastScrollGridView extends GridView implements AbsListView.OnScrollListener {
+	private ScrollListener scrollListener;
+	private int lastFirstVisibleItem = -1;
+	private final Handler fastScrollRemoveHandler = new Handler();
+	private final Runnable fastScrollRemoveTask = () -> RuntimeUtil.runOnUiThread(() -> setFastScrollAlwaysVisible(false));
+
+	public FastScrollGridView(Context context, AttributeSet attrs) {
+		super(new ContextThemeWrapper(context, R.style.Threema_MediaGallery_FastScroll), attrs);
+		setOnScrollListener(this);
+	}
+
+	@Override
+	public void onScrollStateChanged(AbsListView view, int scrollState) {
+		if (scrollState == SCROLL_STATE_IDLE) {
+			fastScrollRemoveHandler.removeCallbacks(fastScrollRemoveTask);
+			fastScrollRemoveHandler.postDelayed(fastScrollRemoveTask, 1000);
+		} else if (scrollState == SCROLL_STATE_TOUCH_SCROLL) {
+			fastScrollRemoveHandler.removeCallbacks(fastScrollRemoveTask);
+			setFastScrollAlwaysVisible(true);
+		}
+	}
+
+	@Override
+	public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+		if (firstVisibleItem != this.lastFirstVisibleItem) {
+			if (this.scrollListener != null) {
+				this.scrollListener.onScroll(firstVisibleItem);
+			}
+			this.lastFirstVisibleItem = firstVisibleItem;
+		}
+	}
+
+	public void setScrollListener(ScrollListener scrollListener) {
+		this.scrollListener = scrollListener;
+	}
+
+	public interface ScrollListener {
+		void onScroll(int firstVisibleItem);
+	}
+}

+ 0 - 67
app/src/main/java/ch/threema/app/ui/OnKeyboardBackRespondingSearchView.java

@@ -1,67 +0,0 @@
-/*  _____ _
- * |_   _| |_  _ _ ___ ___ _ __  __ _
- *   | | | ' \| '_/ -_) -_) '  \/ _` |_
- *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
- *
- * Threema for Android
- * Copyright (c) 2020-2021 Threema GmbH
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package ch.threema.app.ui;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.KeyEvent;
-import androidx.appcompat.widget.SearchView;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class OnKeyboardBackRespondingSearchView extends SearchView {
-	private static final Logger logger = LoggerFactory.getLogger(OnKeyboardBackRespondingSearchView.class);
-
-	private BackPressedListener onImeBack;
-
-	public OnKeyboardBackRespondingSearchView(Context context) {
-		super(context);
-	}
-
-	public OnKeyboardBackRespondingSearchView(Context context, AttributeSet attrs) {
-		super(context, attrs);
-	}
-
-	public OnKeyboardBackRespondingSearchView(Context context, AttributeSet attrs, int defStyleAttr) {
-		super(context, attrs, defStyleAttr);
-	}
-
-	public void setBackPressedListener(BackPressedListener listener) {
-		onImeBack = listener;
-	}
-
-	@Override
-	public boolean dispatchKeyEventPreIme(KeyEvent event) {
-		logger.debug("dispatch pre event triggered" + event);
-		onImeBack.onImeBack(this);
-		if(event.getKeyCode() == KeyEvent.KEYCODE_BACK){
-			logger.debug("Test Back Pressed");
-			onImeBack.onImeBack(this);
-		}
-		return super.dispatchKeyEventPreIme(event);
-	}
-
-	public interface BackPressedListener {
-		void onImeBack(OnKeyboardBackRespondingSearchView etWrite);
-	}
-}

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

@@ -688,11 +688,10 @@ public class AndroidContactUtil {
 	/**
 	 * Create a raw contact for the given identity. Put the identity into the SYNC1 column and set DISPLAY_NAME and data records for messaging and calling
 	 * @param identity
-	 * @param displayName
 	 * @param supportsVoiceCalls
 	 * @return LOOKUP_KEY of the newly created raw contact or null if no contact has been created
 	 */
-	public String createThreemaAndroidContact(String identity, String displayName, boolean supportsVoiceCalls) {
+	public String createThreemaAndroidContact(String identity, boolean supportsVoiceCalls) {
 		Context context = ThreemaApplication.getAppContext();
 		Account account = this.getAccount();
 		if (!TestUtil.required(account, identity)) {

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

@@ -59,8 +59,6 @@ import android.widget.ImageView;
 import android.widget.Toast;
 
 import com.datatheorem.android.trustkit.TrustKit;
-import com.google.android.gms.common.ConnectionResult;
-import com.google.android.gms.common.GoogleApiAvailability;
 import com.google.android.material.snackbar.BaseTransientBottomBar;
 import com.google.android.material.snackbar.Snackbar;
 
@@ -70,6 +68,7 @@ import org.slf4j.LoggerFactory;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.reflect.Method;
+import java.util.Arrays;
 import java.util.Locale;
 
 import javax.net.ssl.SSLSocketFactory;
@@ -571,12 +570,6 @@ public class ConfigUtils {
 		}
 	}
 
-	public static boolean isPlayServicesInstalled(Context context) {
-		GoogleApiAvailability apiAvailability = com.google.android.gms.common.GoogleApiAvailability.getInstance();
-		int resultCode = apiAvailability.isGooglePlayServicesAvailable(context);
-		return RuntimeUtil.isInTest() || (resultCode == ConnectionResult.SUCCESS);
-	}
-
 	public static boolean checkAvailableMemory(float required) {
 		// Get max available VM memory, exceeding this amount will throw an
 		// OutOfMemory exception
@@ -584,7 +577,9 @@ public class ConfigUtils {
 	}
 
 	public static boolean isWorkBuild() {
-		return BuildFlavor.getLicenseType().equals(BuildFlavor.LicenseType.GOOGLE_WORK);
+		return (Arrays.asList(BuildFlavor.LicenseType.GOOGLE_WORK,
+					BuildFlavor.LicenseType.HMS_WORK)
+					.contains(BuildFlavor.getLicenseType()));
 	}
 
 	/**
@@ -619,8 +614,11 @@ public class ConfigUtils {
 	}
 
 	public static boolean isSerialLicensed() {
-		return (BuildFlavor.getLicenseType().equals(BuildFlavor.LicenseType.GOOGLE_WORK) ||
-						BuildFlavor.getLicenseType().equals(BuildFlavor.LicenseType.SERIAL));
+		return (
+			Arrays.asList(BuildFlavor.LicenseType.GOOGLE_WORK,
+				BuildFlavor.LicenseType.HMS_WORK,
+				BuildFlavor.LicenseType.SERIAL)
+				.contains(BuildFlavor.getLicenseType()));
 	}
 
 	/**

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

@@ -45,12 +45,14 @@ import ch.threema.app.services.MessageService;
 import ch.threema.app.services.NotificationService;
 import ch.threema.app.services.ShortcutService;
 import ch.threema.base.ThreemaException;
+import ch.threema.client.file.FileData;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.GroupMessageModel;
 import ch.threema.storage.models.GroupModel;
 import ch.threema.storage.models.MessageModel;
 import ch.threema.storage.models.MessageType;
+import ch.threema.storage.models.data.MessageContentsType;
 
 public class ConversationNotificationUtil {
 	private static final Logger logger = LoggerFactory.getLogger(ConversationNotificationUtil.class);
@@ -229,8 +231,13 @@ public class ConversationNotificationUtil {
 	}
 
 	public static NotificationService.FetchBitmap getFetchThumbnail(final AbstractMessageModel messageModel) {
-		if (messageModel.getType() == MessageType.IMAGE ||
-				messageModel.getType() == MessageType.VIDEO) {
+		if (messageModel.getMessageContentsType() == MessageContentsType.IMAGE ||
+			messageModel.getMessageContentsType() == MessageContentsType.VIDEO
+		) {
+			if (messageModel.getFileData() != null && messageModel.getFileData().getRenderingType() != FileData.RENDERING_MEDIA) {
+				return null;
+			}
+
 			return new NotificationService.FetchBitmap() {
 				@Override
 				public Bitmap fetch() {

+ 11 - 6
app/src/main/java/ch/threema/app/utils/FileUtil.java

@@ -721,21 +721,26 @@ public class FileUtil {
 		}
 
 		try {
+			Intent startIntent;
 			Intent getContentIntent = new Intent();
 			getContentIntent.setType(includeVideo ? MimeUtil.MIME_TYPE_VIDEO: MimeUtil.MIME_TYPE_IMAGE);
 			getContentIntent.setAction(Intent.ACTION_GET_CONTENT);
 			getContentIntent.addCategory(Intent.CATEGORY_OPENABLE);
 			getContentIntent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
 			getContentIntent.putExtra(MediaStore.EXTRA_SIZE_LIMIT, MAX_BLOB_SIZE);
-			Intent pickIntent = new Intent(Intent.ACTION_PICK);
-			pickIntent.setType(MimeUtil.MIME_TYPE_IMAGE);
-			Intent chooserIntent = Intent.createChooser(pickIntent, activity.getString(R.string.select_from_gallery));
-			chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[]{getContentIntent});
 
+			if (includeVideo) {
+				Intent pickIntent = new Intent(Intent.ACTION_PICK);
+				pickIntent.setType(MimeUtil.MIME_TYPE_IMAGE);
+				startIntent = Intent.createChooser(pickIntent, activity.getString(R.string.select_from_gallery));
+				startIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[]{getContentIntent});
+			} else {
+				startIntent = getContentIntent;
+			}
 			if (fragment != null) {
-				fragment.startActivityForResult(chooserIntent, requestCode);
+				fragment.startActivityForResult(startIntent, requestCode);
 			} else {
-				activity.startActivityForResult(chooserIntent, requestCode);
+				activity.startActivityForResult(startIntent, requestCode);
 			}
 		} catch (Exception e) {
 			logger.debug("Exception", e);

+ 33 - 4
app/src/main/java/ch/threema/app/utils/MessageUtil.java

@@ -49,6 +49,7 @@ import ch.threema.storage.models.GroupMessageModel;
 import ch.threema.storage.models.MessageModel;
 import ch.threema.storage.models.MessageState;
 import ch.threema.storage.models.MessageType;
+import ch.threema.storage.models.data.MessageContentsType;
 import ch.threema.storage.models.data.status.VoipStatusDataModel;
 
 public class MessageUtil {
@@ -136,6 +137,22 @@ public class MessageUtil {
 				&& !message.isRead();
 	}
 
+	/**
+	 * return true if the message model can mark as consumed
+	 * @param message
+	 * @return
+	 */
+	public static boolean canMarkAsConsumed(@Nullable AbstractMessageModel message) {
+		return
+			(message instanceof MessageModel || message instanceof GroupMessageModel)
+			&& !message.isStatusMessage()
+			&& !message.isOutbox()
+			&& message.getState() != MessageState.CONSUMED
+			&& (message.getMessageContentsType() == MessageContentsType.VOICE_MESSAGE ||
+				message.getMessageContentsType() == MessageContentsType.AUDIO )
+			&& (message.getState() == null || canChangeToState(message.getState(), MessageState.CONSUMED, message.isOutbox()));
+	}
+
 	/**
 	 * return true, if the user-acknowledge flag can be set
 	 * @param messageModel
@@ -184,16 +201,18 @@ public class MessageUtil {
 				showState = messageState != null
 						&& ((messageModel.isOutbox() && messageState == MessageState.SENDFAILED)
 							|| (messageModel.isOutbox() && messageState == MessageState.SENDING)
-							|| (messageModel.isOutbox() && messageState == MessageState.PENDING && messageModel.getType() != MessageType.BALLOT));
+							|| (messageModel.isOutbox() && messageState == MessageState.PENDING && messageModel.getType() != MessageType.BALLOT)
+							|| (!messageModel.isOutbox() && messageModel.getState() == MessageState.CONSUMED));
 			} else if (messageModel instanceof DistributionListMessageModel) {
 				showState = false;
 			}
 			else if (messageModel instanceof MessageModel) {
 				if(!messageModel.isOutbox()) {
-					//inbox show icon only on acknowledged/declined
+					//inbox show icon only on acknowledged/declined or consumed
 					showState = messageState != null
 							&& (messageModel.getState() == MessageState.USERACK
-							|| messageModel.getState() == MessageState.USERDEC);
+							|| messageModel.getState() == MessageState.USERDEC
+							|| messageModel.getState() == MessageState.CONSUMED);
 				}
 				else {
 					//on outgoing message
@@ -268,7 +287,14 @@ public class MessageUtil {
 		return resolvedReceivers.toArray(new MessageReceiver[resolvedReceivers.size()]);
 	}
 
-	public static boolean canChangeToState(MessageState fromState, MessageState toState, boolean isOutbox) {
+	/**
+	 * Check if a MessageState change from fromState to toState is allowed
+	 * @param fromState State from which a state change is requested
+	 * @param toState State to which a state change is requested
+	 * @param isOutbox true, if it's an outgoing message
+	 * @return true if a state change is allowed, false otherwise
+	 */
+	public static boolean canChangeToState(@Nullable MessageState fromState, @Nullable MessageState toState, boolean isOutbox) {
 		if (fromState == null || toState == null) {
 			//invalid data
 			return false;
@@ -303,6 +329,9 @@ public class MessageUtil {
 				return true;
 			case USERDEC:
 				return true;
+			case CONSUMED:
+				return fromState != MessageState.USERACK
+					&& fromState != MessageState.USERDEC;
 			case PENDING:
 				return fromState == MessageState.SENDFAILED;
 			case SENDING:

+ 381 - 40
app/src/main/java/ch/threema/app/utils/PushUtil.java

@@ -27,77 +27,418 @@ import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
 import android.os.Build;
 import android.text.format.DateUtils;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.Map;
+
+import androidx.annotation.Nullable;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
 import androidx.preference.PreferenceManager;
-import ch.threema.app.FcmRegistrationIntentService;
+import androidx.work.Constraints;
+import androidx.work.Data;
+import androidx.work.NetworkType;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.WorkManager;
+import androidx.work.WorkRequest;
 import ch.threema.app.R;
-import ch.threema.app.jobs.FcmRegistrationJobService;
+import ch.threema.app.ThreemaApplication;
+import ch.threema.app.jobs.ReConnectJobService;
+import ch.threema.app.managers.ServiceManager;
+import ch.threema.app.push.PushRegistrationWorker;
+import ch.threema.app.receivers.AlarmManagerBroadcastReceiver;
+import ch.threema.app.services.DeadlineListService;
+import ch.threema.app.services.LockAppService;
+import ch.threema.app.services.NotificationService;
+import ch.threema.app.services.NotificationServiceImpl;
+import ch.threema.app.services.PollingHelper;
+import ch.threema.app.services.PreferenceService;
+import ch.threema.app.services.PreferenceServiceImpl;
+import ch.threema.app.services.RingtoneService;
+import ch.threema.app.stores.PreferenceStore;
+import ch.threema.app.webclient.services.SessionWakeUpServiceImpl;
+import ch.threema.base.ThreemaException;
+import ch.threema.client.ThreemaConnection;
 
 public class PushUtil {
 	private static final Logger logger = LoggerFactory.getLogger(PushUtil.class);
 
+	public static final String EXTRA_CLEAR_TOKEN = "clear";
+	public static final String EXTRA_WITH_CALLBACK = "cb";
+	public static final String EXTRA_REGISTRATION_ERROR_BROADCAST = "rer";
+
+	private static final String WEBCLIENT_SESSION = "wcs";
+	private static final String WEBCLIENT_TIMESTAMP = "wct";
+	private static final String WEBCLIENT_VERSION = "wcv";
+	private static final String WEBCLIENT_AFFILIATION_ID = "wca";
+
+	private static final int RECONNECT_JOB = 89;
+
 	/**
-	 * Clears the "token last updated" setting in shared preferences
+	 * Send push token to server
 	 * @param context Context
+	 * @param clear Remove token from sever
+	 * @param withCallback Send broadcast after token refresh has been completed or failed
 	 */
-	public static void clearPushTokenSentDate(Context context) {
-		SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
-		if (sharedPreferences != null) {
-			sharedPreferences
-					.edit()
-					.putLong(context.getString(R.string.preferences__gcm_token_sent_date), 0L)
-					.apply();
+	public static void enqueuePushTokenUpdate(Context context, boolean clear, boolean withCallback) {
+		Data workerFlags = new Data.Builder()
+			.putBoolean(EXTRA_CLEAR_TOKEN, clear)
+			.putBoolean(EXTRA_WITH_CALLBACK, withCallback)
+			.build();
+
+		Constraints workerConstraints = new Constraints.Builder()
+			.setRequiredNetworkType(NetworkType.CONNECTED)
+			.build();
+
+		// worker differs between hms and regular builds, see gcm and hms directory for for overwriting push worker versions
+		WorkRequest pushTokenRegistrationRequest = new OneTimeWorkRequest.Builder(PushRegistrationWorker.class)
+			.setInputData(workerFlags)
+			.setConstraints(workerConstraints)
+			.build();
+
+		WorkManager.getInstance(context).enqueue(pushTokenRegistrationRequest);
+	}
+
+	/**
+	 * Send a push token to the server
+	 * @param context Context to access shared preferences and key strings for the the last token sent date update
+	 * @param token String representing the token
+	 * @param type int representing the token type (gcm, hms or none in case of a reset)
+	 */
+	public static void sendTokenToServer(Context context, String token, int type) throws ThreemaException {
+		ServiceManager serviceManager = ThreemaApplication.getServiceManager();
+
+		if (serviceManager != null) {
+			ThreemaConnection connection = serviceManager.getConnection();
+
+			if (connection != null) {
+				connection.setPushToken(type, token);
+				logger.info("push token of type {} successfully sent to server", type);
+
+				// reset token update timestamp if it was reset, set current update time otherwise
+				if (token.isEmpty()) {
+					PreferenceManager.getDefaultSharedPreferences(context)
+						.edit()
+						.putLong(ThreemaApplication.getAppContext().getString(R.string.preferences__token_sent_date), 0L)
+						.apply();
+				}
+				else {
+					PreferenceManager.getDefaultSharedPreferences(context)
+						.edit()
+						.putLong(context.getString(R.string.preferences__token_sent_date), System.currentTimeMillis())
+						.apply();
+				}
+				// Used in the Webclient Sessions
+				serviceManager.getPreferenceService().setPushToken(token);
+			} else {
+				throw new ThreemaException("Unable to send / clear push token. ThreemaConnection not available");
+			}
+		} else {
+			throw new ThreemaException("Unable to send / clear push token. ServiceManager not available");
 		}
 	}
 
 	/**
-	 * Directly send push token to server
-	 * @param context Context
-	 * @param clear Remove token from sever
-	 * @param withCallback Send broadcast after token refresh has been completed or failed
+	 * Signal a push token update through a local broadcast
+	 * @param error String potential error message
+	 * @param clearToken boolean whether the token was reset
 	 */
-	public static void sendPushTokenToServer(Context context, boolean clear, boolean withCallback) {
-		logger.debug("Update FCM token now");
-		// Start IntentService to register this application with FCM.
-		Intent intent = new Intent(context, FcmRegistrationIntentService.class);
-		if (clear) {
-			intent.putExtra(FcmRegistrationIntentService.EXTRA_CLEAR_TOKEN, true);
+	public static void signalRegistrationFinished(@Nullable String error, boolean clearToken) {
+		final Intent intent = new Intent(ThreemaApplication.INTENT_PUSH_REGISTRATION_COMPLETE);
+		if (error != null) {
+			logger.error("Failed to get push token {}", error);
+			intent.putExtra(PushUtil.EXTRA_REGISTRATION_ERROR_BROADCAST, true);
 		}
-		if (withCallback) {
-			intent.putExtra(FcmRegistrationIntentService.EXTRA_WITH_CALLBACK, true);
+		else {
+			intent.putExtra(PushUtil.EXTRA_CLEAR_TOKEN, clearToken);
 		}
-		FcmRegistrationIntentService.enqueueWork(context, intent);
+		LocalBroadcastManager.getInstance(ThreemaApplication.getAppContext()).sendBroadcast(intent);
 	}
 
 	/**
-	 * Schedule a push token refresh as soon as the device has become online
-	 * Will perform an instant refresh on Jelly Bean and Kitkat
-	 * @param context Context
+	 * Process the Data mapping received from a FCM message
+	 * @param data Map<String, String> key value pairs with webclient session infos
 	 */
-	public static void scheduleSendPushTokenToServer(Context context) {
-		if (!ConfigUtils.isPlayServicesInstalled(context)) {
-			return;
+	public static void processRemoteMessage(Map<String, String> data) {
+		logger.info("processRemoteMessage");
+
+		// Webclient push
+		if (data != null && data.containsKey(WEBCLIENT_SESSION) && data.containsKey(WEBCLIENT_TIMESTAMP)) {
+			sendWebclientNotification(data);
+		} else { // New messages push, trigger a reconnect and show new message notification(s)
+			sendNotification();
 		}
+	}
+
+	private static void sendNotification() {
+		logger.info("sendNotification");
+		Context appContext = ThreemaApplication.getAppContext();
+		PollingHelper pollingHelper = new PollingHelper(appContext, "FCM");
+
+		ConnectivityManager mgr = (ConnectivityManager) appContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+		NetworkInfo networkInfo = mgr.getActiveNetworkInfo();
+		if (networkInfo != null && networkInfo.getDetailedState() == NetworkInfo.DetailedState.BLOCKED) {
+			logger.warn("Network blocked (background data disabled?)");
+			// The same message may arrive twice (due to a network change). so we simply ignore messages that we were unable to fetch due to a blocked network
+			// Simply schedule a poll when the device is back online
+			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+				JobScheduler js = (JobScheduler) appContext.getSystemService(Context.JOB_SCHEDULER_SERVICE);
 
-		logger.debug("Scheduling FCM token update");
+				js.cancel(RECONNECT_JOB);
 
-		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, FcmRegistrationJobService.class);
-				JobInfo.Builder builder = new JobInfo.Builder(9991, serviceComponent);
-				builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
-				jobScheduler.schedule(builder.build());
-				return;
+				JobInfo job = new JobInfo.Builder(RECONNECT_JOB,
+					new ComponentName(appContext, ReConnectJobService.class))
+					.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
+					.setRequiresCharging(false)
+					.build();
+
+				if (js.schedule(job) != JobScheduler.RESULT_SUCCESS) {
+					logger.error("Job scheduling failed");
+				}
 			}
+			return;
 		}
 
-		sendPushTokenToServer(context, false, false);
+		if (networkInfo == null) {
+			logger.warn("No network info available");
+		}
+
+		//recheck after one minute
+		AlarmManagerBroadcastReceiver.requireLoggedInConnection(
+			appContext,
+			(int) DateUtils.MINUTE_IN_MILLIS
+		);
+
+		PreferenceStore preferenceStore = new PreferenceStore(appContext, null);
+		PreferenceServiceImpl preferenceService = new PreferenceServiceImpl(appContext, preferenceStore);
+
+		if (ThreemaApplication.getMasterKey() != null &&
+			ThreemaApplication.getMasterKey().isLocked() &&
+			preferenceService.isMasterKeyNewMessageNotifications()) {
+
+			displayAdHocNotification();
+		}
+
+		if (!pollingHelper.poll(true)) {
+			logger.warn("Unable to establish connection");
+		}
+	}
+
+	private static void displayAdHocNotification() {
+		logger.info("displayAdHocNotification");
+		final Context appContext = ThreemaApplication.getAppContext();
+		final ServiceManager serviceManager = ThreemaApplication.getServiceManager();
+
+		NotificationService notificationService;
+		if (serviceManager != null) {
+			notificationService = serviceManager.getNotificationService();
+		} else {
+			// Because the master key is locked, there is no preference service object.
+			// We need to create one for ourselves so that we can read the user's notification prefs
+			// (which are unencrypted).
+			//create a temporary service class (with some implementations) to use the showMasterKeyLockedNewMessageNotification
+			PreferenceStore ps = new PreferenceStore(appContext, ThreemaApplication.getMasterKey());
+			PreferenceService p = new PreferenceServiceImpl(appContext, ps);
+
+			notificationService = new NotificationServiceImpl(
+				appContext,
+				new LockAppService() {
+					@Override
+					public boolean isLockingEnabled() {
+						return false;
+					}
+
+					@Override
+					public boolean unlock(String pin) {
+						return false;
+					}
+
+					@Override
+					public void lock() {
+						//not needed in this context
+					}
+
+					@Override
+					public boolean checkLock() {
+						return false;
+					}
+
+					@Override
+					public boolean isLocked() {
+						return false;
+					}
+
+					@Override
+					public LockAppService resetLockTimer(boolean restartAfterReset) {
+						return null;
+					}
+
+					@Override
+					public void addOnLockAppStateChanged(OnLockAppStateChanged c) {
+						//not needed in this context
+					}
+
+					@Override
+					public void removeOnLockAppStateChanged(OnLockAppStateChanged c) {
+						//not needed in this context
+					}
+				},
+				new DeadlineListService() {
+					@Override
+					public void add(String uid, long timeout) {
+						//not needed in this context
+					}
+
+					@Override
+					public void init() {
+						//not needed in this context
+					}
+
+					@Override
+					public boolean has(String uid) {
+						return false;
+					}
+
+					@Override
+					public void remove(String uid) {
+						//not needed in this context
+					}
+
+					@Override
+					public long getDeadline(String uid) {
+						return 0;
+					}
+
+					@Override
+					public int getSize() {
+						return 0;
+					}
+
+					@Override
+					public void clear() {
+						//not needed in this context
+					}
+				},
+				p,
+				new RingtoneService() {
+					@Override
+					public void init() {
+						//not needed in this context
+					}
+
+					@Override
+					public void setRingtone(String uniqueId, Uri ringtoneUri) {
+						//not needed in this context
+					}
+
+					@Override
+					public Uri getRingtoneFromUniqueId(String uniqueId) {
+						return null;
+					}
+
+					@Override
+					public boolean hasCustomRingtone(String uniqueId) {
+						return false;
+					}
+
+					@Override
+					public void removeCustomRingtone(String uniqueId) {
+						//not needed in this context
+					}
+
+					@Override
+					public void resetRingtones(Context context) {
+						//not needed in this context
+					}
+
+					@Override
+					public Uri getContactRingtone(String uniqueId) {
+						return null;
+					}
+
+					@Override
+					public Uri getGroupRingtone(String uniqueId) {
+						return null;
+					}
+
+					@Override
+					public Uri getVoiceCallRingtone(String uniqueId) {
+						return null;
+					}
+
+					@Override
+					public Uri getDefaultContactRingtone() {
+						return null;
+					}
+
+					@Override
+					public Uri getDefaultGroupRingtone() {
+						return null;
+					}
+
+					@Override
+					public boolean isSilent(String uniqueId, boolean isGroup) {
+						return false;
+					}
+				});
+		}
+
+		if (notificationService != null) {
+			notificationService.showMasterKeyLockedNewMessageNotification();
+		}
+	}
+
+	private static void sendWebclientNotification(Map<String, String> data) {
+		final String session = data.get(WEBCLIENT_SESSION);
+		final String timestamp = data.get(WEBCLIENT_TIMESTAMP);
+		final String version = data.get(WEBCLIENT_VERSION);
+		final String affiliationId = data.get(WEBCLIENT_AFFILIATION_ID);
+		if (session != null && !session.isEmpty() && timestamp != null && !timestamp.isEmpty()) {
+			logger.debug("Received webclient wakeup for session {}", session);
+
+			final Thread t = new Thread(() -> {
+				logger.info("Trying to wake up webclient session {}", session);
+
+				// Parse version number
+				Integer versionNumber = null;
+				if (version != null) { // Can be null during beta, if an old client doesn't yet send the version field
+					try {
+						versionNumber = Integer.parseInt(version);
+					} catch (NumberFormatException e) {
+						// Version number was sent but is not a valid u16.
+						// We should probably throw the entire wakeup notification away.
+						logger.error("Could not parse webclient protocol version number: ", e);
+						return;
+					}
+				}
+
+				// Try to wake up session
+				SessionWakeUpServiceImpl.getInstance()
+					.resume(session, versionNumber == null ? 0 : versionNumber, affiliationId);
+			});
+			t.setName("webclient-wakeup");
+			t.start();
+		}
+	}
+
+	/**
+	 * Clear the "token last updated" setting in shared preferences
+	 * @param context Context
+	 */
+	public static void clearPushTokenSentDate(Context context) {
+		SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+		if (sharedPreferences != null) {
+			sharedPreferences
+					.edit()
+					.putLong(context.getString(R.string.preferences__token_sent_date), 0L)
+					.apply();
+		}
 	}
 
 	/**
@@ -109,7 +450,7 @@ public class PushUtil {
 		SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
 
 		if (sharedPreferences != null) {
-			long lastDate = sharedPreferences.getLong(context.getString(R.string.preferences__gcm_token_sent_date), 0L);
+			long lastDate = sharedPreferences.getLong(context.getString(R.string.preferences__token_sent_date), 0L);
 			// refresh push token at least once a day
 			return (System.currentTimeMillis() - lastDate) > DateUtils.DAY_IN_MILLIS;
 		}

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

@@ -72,6 +72,7 @@ public class StateBitmapUtil {
 		this.messageStateBitmapResourceIds.put(MessageState.SENDING, R.drawable.ic_upload_filled);
 		this.messageStateBitmapResourceIds.put(MessageState.PENDING, R.drawable.ic_upload_filled);
 		this.messageStateBitmapResourceIds.put(MessageState.TRANSCODING, R.drawable.ic_outline_hourglass_top_24);
+		this.messageStateBitmapResourceIds.put(MessageState.CONSUMED, R.drawable.ic_baseline_hearing_24);
 
 		this.messageStateDescriptionMap.put(MessageState.READ, R.string.state_read);
 		this.messageStateDescriptionMap.put(MessageState.DELIVERED, R.string.state_delivered);
@@ -82,6 +83,7 @@ public class StateBitmapUtil {
 		this.messageStateDescriptionMap.put(MessageState.SENDING, R.string.state_sending);
 		this.messageStateDescriptionMap.put(MessageState.PENDING, R.string.state_pending);
 		this.messageStateDescriptionMap.put(MessageState.TRANSCODING, R.string.state_processing);
+		this.messageStateDescriptionMap.put(MessageState.CONSUMED, R.string.listened_to);
 
 		this.ackColor = context.getResources().getColor(R.color.material_green);
 		this.decColor = context.getResources().getColor(R.color.material_orange);

+ 20 - 0
app/src/main/java/ch/threema/app/voip/CallState.java

@@ -79,6 +79,13 @@ public class CallState {
 	private final AtomicInteger state = new AtomicInteger(IDLE);
 	private final AtomicLong callId = new AtomicLong(0);
 
+	/**
+	 * Whether or not an answer for this call ID has been received yet.
+	 *
+	 * This flag is reset when the state transitions to DISCONNECTING or IDLE.
+	 */
+	private volatile boolean answerReceived = false;
+
 	/**
 	 * The incoming call counter is a transitional variable that is used as long as the call ID has
 	 * not yet been fully rolled out. It is being used to avoid problems if two calls are using the
@@ -134,6 +141,13 @@ public class CallState {
 		return this.incomingCallCounter.get();
 	}
 
+	/**
+	 * Return whether an answer was already received for this call.
+	 */
+	public synchronized boolean answerReceived() {
+		return this.answerReceived;
+	}
+
 	/**
 	 * Return an immutable snapshot of the current state.
 	 * This allows reading the state and the Call ID independently without locking.
@@ -173,6 +187,7 @@ public class CallState {
 	public synchronized void setIdle() {
 		this.state.set(IDLE);
 		this.callId.set(0);
+		this.answerReceived = false;
 	}
 
 	public synchronized void setRinging(long callId) {
@@ -204,6 +219,10 @@ public class CallState {
 		this.callId.set(callId);
 	}
 
+	public synchronized void setAnswerReceived() {
+		this.answerReceived = true;
+	}
+
 	public synchronized void setCalling(long callId) {
 		final @State int state = this.state.get();
 		if (state != INITIALIZING) {
@@ -230,6 +249,7 @@ public class CallState {
 			logger.warn("Call ID changed from {} to {}", oldCallId, callId);
 		}
 		this.callId.set(callId);
+		this.answerReceived = false;
 	}
 
 	//endregion

+ 12 - 14
app/src/main/java/ch/threema/app/voip/PeerConnectionClient.java

@@ -131,11 +131,9 @@ public class PeerConnectionClient {
 
 	private static final String AUDIO_TRACK_ID = "3MACALLa0";
 	private static final String AUDIO_CODEC_OPUS = "opus";
-	private static final String AUDIO_CODEC_PARAM_BITRATE = "maxaveragebitrate";
 	private static final String AUDIO_LEVEL_CONTROL_CONSTRAINT = "levelControl";
 
 	private static final String VIDEO_TRACK_ID = "3MACALLv0";
-	private static final String VIDEO_TRACK_TYPE = "video";
 
 	// Capturing settings. What's being sent may be lower.
 	private static final int VIDEO_WIDTH = 1920;
@@ -257,13 +255,13 @@ public class PeerConnectionClient {
 		 * Initialize the peer connection client.
 		 *
 		 * @param tracing Enable WebRTC trace logging. Should only be used for internal debugging.
-		 * @param useOpenSLES
+		 * @param useOpenSLES Use OpenSL ES
 		 * @param disableBuiltInAEC Disable acoustic echo cancelation
 		 * @param disableBuiltInAGC Disable automatic gain control
 		 * @param disableBuiltInNS Disable noise suppression
-		 * @param enableLevelControl
-		 * @param videoCallEnabled
-		 * @param videoCodecHwAcceleration
+		 * @param enableLevelControl Enable level control
+		 * @param videoCallEnabled Enable video calls
+		 * @param videoCodecHwAcceleration Enable video codec hardware acceleration
 		 * @param rtpHeaderExtensionConfig See {@link SdpPatcher}
 		 * @param forceTurn Whether TURN servers should be forced (relay only).
 		 * @param gatherContinually Whether ICE candidates should be gathered continually.
@@ -431,8 +429,6 @@ public class PeerConnectionClient {
 
 	/**
 	 * Enable or disable the use of ICE servers (defaults to enabled).
-	 *
-	 * @param enableIceServers
 	 */
 	public void setEnableIceServers(boolean enableIceServers) {
 		this.enableIceServers = enableIceServers;
@@ -493,22 +489,23 @@ public class PeerConnectionClient {
 
 	/**
 	 * Create the peer connection factory.
-	 * @return true if the factory was created, false otherwise.
+	 *
+	 * The future completes with true if the factory was created, false otherwise.
 	 */
 	@WorkerThread
-	private boolean createPeerConnectionFactoryInternal(@NonNull CompletableFuture<Boolean> future) {
+	private void createPeerConnectionFactoryInternal(@NonNull CompletableFuture<Boolean> future) {
 		logger.info("Create peer connection factory");
 		if (this.factory != null) {
 			logger.error("Peer connetion factory already initialized");
 			future.complete(false);
-			return false;
+			return;
 		}
 		try {
 			this.factoryInitializing.acquire();
 		} catch (InterruptedException e) {
 			logger.error("Interrupted while acquiring semaphore", e);
 			future.complete(false);
-			return false;
+			return;
 		}
 
 		this.isError = false;
@@ -605,7 +602,6 @@ public class PeerConnectionClient {
 
 		this.factoryInitializing.release();
 		future.complete(true);
-		return true;
 	}
 
 	@WorkerThread
@@ -1915,7 +1911,9 @@ public class PeerConnectionClient {
 		@Override
 		public void onFirstFrameAvailable() {
 			logger.debug("Camera first frame available");
-			events.onCameraFirstFrameAvailable();
+			if (events != null) {
+				events.onCameraFirstFrameAvailable();
+			}
 		}
 
 		@Override

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

@@ -94,8 +94,10 @@ import androidx.transition.ChangeBounds;
 import androidx.transition.Transition;
 import androidx.transition.TransitionManager;
 import ch.threema.app.BuildConfig;
+import ch.threema.app.push.PushService;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
+import ch.threema.app.wearable.WearableHandler;
 import ch.threema.app.activities.ThreemaActivity;
 import ch.threema.app.dialogs.BottomSheetAbstractDialog;
 import ch.threema.app.dialogs.BottomSheetListDialog;
@@ -1674,8 +1676,8 @@ public class CallActivity extends ThreemaActivity implements
 		final Intent answerIntent = new Intent(getIntent());
 		answerIntent.setClass(getApplicationContext(), VoipCallService.class);
 		ContextCompat.startForegroundService(this, answerIntent);
-		if (ConfigUtils.isPlayServicesInstalled(getApplicationContext())){
-			this.voipStateService.cancelOnWearable(VoipStateService.TYPE_ACTIVITY);
+		if (PushService.playServicesInstalled(getApplicationContext())){
+			WearableHandler.cancelOnWearable(VoipStateService.TYPE_ACTIVITY);
 		}
 	}
 

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

@@ -80,6 +80,7 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
 import androidx.preference.PreferenceManager;
 import ch.threema.annotation.SameThread;
 import ch.threema.app.BuildConfig;
+import ch.threema.app.push.PushService;
 import ch.threema.app.R;
 import ch.threema.app.exceptions.FileSystemNotPresentException;
 import ch.threema.app.managers.ListenerManager;
@@ -115,6 +116,7 @@ import ch.threema.app.voip.util.VideoCapturerUtil;
 import ch.threema.app.voip.util.VoipStats;
 import ch.threema.app.voip.util.VoipUtil;
 import ch.threema.app.voip.util.VoipVideoParams;
+import ch.threema.app.wearable.WearableHandler;
 import ch.threema.base.ThreemaException;
 import ch.threema.base.VerificationLevel;
 import ch.threema.client.ThreemaFeature;
@@ -616,8 +618,8 @@ public class VoipCallService extends LifecycleService implements PeerConnectionC
 
 		// if the intent creation was initiated from the phone we additionally cancel a potentially already opened activity on the watch
 		final boolean cancelActivityOnWearable = intent.getBooleanExtra(EXTRA_CANCEL_WEAR, false);
-		if (cancelActivityOnWearable && ConfigUtils.isPlayServicesInstalled(getAppContext())) {
-			voipStateService.cancelOnWearable(VoipStateService.TYPE_ACTIVITY);
+		if (cancelActivityOnWearable && PushService.playServicesInstalled(getAppContext())) {
+			WearableHandler.cancelOnWearable(VoipStateService.TYPE_ACTIVITY);
 		}
 
 		final VoipICECandidatesData candidatesData =
@@ -789,8 +791,8 @@ public class VoipCallService extends LifecycleService implements PeerConnectionC
 				}
 			}.execute(new Pair<>(contact, callState.getCallId()));
 		}
-		if (ConfigUtils.isPlayServicesInstalled(getAppContext())){
-			voipStateService.cancelOnWearable(VoipStateService.TYPE_ACTIVITY);
+		if (PushService.playServicesInstalled(getAppContext())){
+			WearableHandler.cancelOnWearable(VoipStateService.TYPE_ACTIVITY);
 		}
 		disconnect();
 	}
@@ -1479,11 +1481,11 @@ public class VoipCallService extends LifecycleService implements PeerConnectionC
 					}
 				});
 			}
-			this.voipStateService.cancelOnWearable(VoipStateService.TYPE_ACTIVITY);
+			WearableHandler.cancelOnWearable(VoipStateService.TYPE_ACTIVITY);
 		}
 
-		if (ConfigUtils.isPlayServicesInstalled(getAppContext())){
-			voipStateService.cancelOnWearable(VoipStateService.TYPE_ACTIVITY);
+		if (PushService.playServicesInstalled(getAppContext())){
+			WearableHandler.cancelOnWearable(VoipStateService.TYPE_ACTIVITY);
 		}
 
 		this.preDisconnect(callId);
@@ -1667,8 +1669,8 @@ public class VoipCallService extends LifecycleService implements PeerConnectionC
 	@AnyThread
 	private synchronized void abortCall(@StringRes final int userMessage, @Nullable final String internalMessage, boolean showErrorNotification) {
 		this.abortCall(userMessage, internalMessage, null, showErrorNotification);
-		if (ConfigUtils.isPlayServicesInstalled(getAppContext())){
-			voipStateService.cancelOnWearable(VoipStateService.TYPE_ACTIVITY);
+		if (PushService.playServicesInstalled(getAppContext())){
+			WearableHandler.cancelOnWearable(VoipStateService.TYPE_ACTIVITY);
 		}
 	}
 

+ 35 - 143
app/src/main/java/ch/threema/app/voip/services/VoipStateService.java

@@ -41,23 +41,11 @@ import android.text.Spannable;
 import android.text.SpannableString;
 import android.text.style.ForegroundColorSpan;
 
-import com.google.android.gms.common.data.FreezableUtils;
-import com.google.android.gms.tasks.Tasks;
-import com.google.android.gms.wearable.DataClient;
-import com.google.android.gms.wearable.DataEvent;
-import com.google.android.gms.wearable.DataEventBuffer;
-import com.google.android.gms.wearable.DataMapItem;
-import com.google.android.gms.wearable.Node;
-import com.google.android.gms.wearable.PutDataMapRequest;
-import com.google.android.gms.wearable.PutDataRequest;
-import com.google.android.gms.wearable.Wearable;
-
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.webrtc.IceCandidate;
 import org.webrtc.SessionDescription;
 
-import java.io.ByteArrayOutputStream;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
@@ -66,7 +54,6 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.concurrent.ExecutionException;
 
 import androidx.annotation.AnyThread;
 import androidx.annotation.IntDef;
@@ -80,6 +67,7 @@ import ch.threema.app.R;
 import ch.threema.app.messagereceiver.ContactMessageReceiver;
 import ch.threema.app.messagereceiver.MessageReceiver;
 import ch.threema.app.notifications.NotificationBuilderWrapper;
+import ch.threema.app.push.PushService;
 import ch.threema.app.services.ContactService;
 import ch.threema.app.services.LifetimeService;
 import ch.threema.app.services.MessageService;
@@ -90,7 +78,6 @@ import ch.threema.app.utils.DNDUtil;
 import ch.threema.app.utils.IdUtil;
 import ch.threema.app.utils.MediaPlayerStateWrapper;
 import ch.threema.app.utils.NameUtil;
-import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.voip.CallState;
 import ch.threema.app.voip.CallStateSnapshot;
 import ch.threema.app.voip.Config;
@@ -99,6 +86,7 @@ import ch.threema.app.voip.managers.VoipListenerManager;
 import ch.threema.app.voip.receivers.CallRejectReceiver;
 import ch.threema.app.voip.receivers.VoipMediaButtonReceiver;
 import ch.threema.app.voip.util.VoipUtil;
+import ch.threema.app.wearable.WearableHandler;
 import ch.threema.base.ThreemaException;
 import ch.threema.client.MessageQueue;
 import ch.threema.client.voip.VoipCallAnswerData;
@@ -116,6 +104,7 @@ import ch.threema.storage.models.ContactModel;
 import java8.util.concurrent.CompletableFuture;
 
 import static ch.threema.app.ThreemaApplication.INCOMING_CALL_NOTIFICATION_ID;
+import static ch.threema.app.ThreemaApplication.getAppContext;
 import static ch.threema.app.notifications.NotificationBuilderWrapper.VIBRATE_PATTERN_INCOMING_CALL;
 import static ch.threema.app.notifications.NotificationBuilderWrapper.VIBRATE_PATTERN_SILENT;
 import static ch.threema.app.services.NotificationService.NOTIFICATION_CHANNEL_CALL;
@@ -199,44 +188,7 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 	private static final int RINGING_TIMEOUT_SECONDS = 60;
 	private static final int VOIP_CONNECTION_LINGER = 1000 * 5;
 
-	private DataClient.OnDataChangedListener wearableListener = new DataClient.OnDataChangedListener() {
-		@Override
-		public void onDataChanged(@NonNull DataEventBuffer eventsBuffer) {
-			logger.debug("onDataChanged Listener VoipState: " + eventsBuffer);
-			final List<DataEvent> events = FreezableUtils.freezeIterable(eventsBuffer);
-			for (DataEvent event : events) {
-				if (event.getType() == DataEvent.TYPE_CHANGED) {
-					String path = event.getDataItem().getUri().getPath();
-					logger.debug("received datachange path " + path);
-					if ("/accept-call".equals(path)) {
-						logger.debug("accept call block entered");
-						DataMapItem dataMapItem = DataMapItem.fromDataItem(event.getDataItem());
-						long callId = dataMapItem.getDataMap().getLong(EXTRA_CALL_ID, 0L);
-						String identity = dataMapItem.getDataMap().getString(EXTRA_CONTACT_IDENTITY);
-						final Intent intent = createAcceptIntent(callId, identity);
-						appContext.startService(intent);
-						//Listen again for hang up
-						Wearable.getDataClient(appContext).addListener(wearableListener);
-
-					} if("/reject-call".equals(path)) {
-						logger.debug("reject call block entered");
-						DataMapItem dataMapItem = DataMapItem.fromDataItem(event.getDataItem());
-						long callId = dataMapItem.getDataMap().getLong(EXTRA_CALL_ID, 0L);
-						String identity = dataMapItem.getDataMap().getString(EXTRA_CONTACT_IDENTITY);
-						final Intent rejectIntent = createRejectIntent(
-							callId,
-							identity,
-							VoipCallAnswerData.RejectReason.REJECTED
-						);
-						CallRejectService.enqueueWork(appContext, rejectIntent);
-					} if ("/disconnect-call".equals(path)){
-						logger.debug("disconnect call block entered");
-						VoipUtil.sendVoipCommand(appContext, VoipCallService.class, VoipCallService.ACTION_HANGUP);
-					}
-				}
-			}
-		}
-	};
+	private final WearableHandler wearableHandler;
 
 	public VoipStateService(ContactService contactService,
 	                        RingtoneService ringtoneService,
@@ -256,6 +208,7 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 		this.notificationManagerCompat = NotificationManagerCompat.from(appContext);
 		this.notificationManager = (NotificationManager) appContext.getSystemService(Context.NOTIFICATION_SERVICE);
 		this.audioManager = (AudioManager) appContext.getSystemService(Context.AUDIO_SERVICE);
+		this.wearableHandler = new WearableHandler(appContext);
 	}
 
 	//region Logging
@@ -513,8 +466,8 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 	/**
 	 * Create a new accept intent for the specified call ID / identity.
 	 */
-	private Intent createAcceptIntent(long callId, @NonNull String identity) {
-		final Intent intent = new Intent(appContext, VoipCallService.class);
+	public static Intent createAcceptIntent(long callId, @NonNull String identity) {
+		final Intent intent = new Intent(getAppContext(), VoipCallService.class);
 		intent.putExtra(EXTRA_CALL_ID, callId);
 		intent.putExtra(EXTRA_CONTACT_IDENTITY, identity);
 		intent.putExtra(EXTRA_IS_INITIATOR, false);
@@ -524,8 +477,8 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 	/**
 	 * Create a new reject intent for the specified call ID / identity.
 	 */
-	private Intent createRejectIntent(long callId, @NonNull String identity, byte rejectReason) {
-		final Intent intent = new Intent(this.appContext, CallRejectReceiver.class);
+	public static Intent createRejectIntent(long callId, @NonNull String identity, byte rejectReason) {
+		final Intent intent = new Intent(getAppContext(), CallRejectReceiver.class);
 		intent.putExtra(EXTRA_CALL_ID, callId);
 		intent.putExtra(EXTRA_CONTACT_IDENTITY, identity);
 		intent.putExtra(EXTRA_IS_INITIATOR, false);
@@ -679,7 +632,7 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 		// Set state to RINGING
 		this.setStateRinging(callId);
 
-		// play ringtone
+		// Play ringtone
 		this.playRingtone(messageReceiver, isMuted);
 
 		// Show call notification
@@ -762,9 +715,15 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 				return true;
 			}
 
+			// Ensure that an answer wasn't already received
+			if (this.callState.answerReceived()) {
+				logCallWarning(callId, "Received extra answer, ignoring");
+				return true;
+			}
+
 			// Ensure that action was set
 			if (callAnswerData.getAction() == null) {
-			    logger.error("Call answer received without action, ignoring");
+			    logCallWarning(callId, "Call answer received without action, ignoring");
 			    return true;
 			}
 
@@ -791,15 +750,18 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 					logCallInfo(callId, "Call answer received from {}: Unknown action: {}", callAnswerData.getAction());
 					break;
 			}
-		}
 
-		// Notify listeners
-		VoipListenerManager.messageListener.handle(listener -> {
-			final String identity = msg.getFromIdentity();
-			if (listener.handle(identity)) {
-				listener.onAnswer(identity, callAnswerData);
-			}
-		});
+			// Mark answer as received
+			this.callState.setAnswerReceived();
+
+			// Notify listeners
+			VoipListenerManager.messageListener.handle(listener -> {
+				final String identity = msg.getFromIdentity();
+				if (listener.handle(identity)) {
+					listener.onAnswer(identity, callAnswerData);
+				}
+			});
+		}
 
 		return true;
 	}
@@ -1343,9 +1305,9 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 				logger.warn("No call notification found for {}", identity);
 			}
 		}
-		if (ConfigUtils.isPlayServicesInstalled(appContext)){
-			cancelOnWearable(TYPE_NOTIFICATION);
-			cancelOnWearable(TYPE_ACTIVITY);
+		if (PushService.playServicesInstalled(appContext)){
+			WearableHandler.cancelOnWearable(TYPE_NOTIFICATION);
+			WearableHandler.cancelOnWearable(TYPE_ACTIVITY);
 		}
 	}
 
@@ -1360,50 +1322,11 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 			}
 			this.callNotificationTags.clear();
 		}
-		if (ConfigUtils.isPlayServicesInstalled(appContext)){
-			cancelOnWearable(TYPE_NOTIFICATION);
+		if (PushService.playServicesInstalled(appContext)){
+			WearableHandler.cancelOnWearable(TYPE_NOTIFICATION);
 		}
 	}
 
-	public void cancelOnWearable(@Component int component){
-		RuntimeUtil.runInAsyncTask(() -> {
-			try {
-				final List<Node> nodes = Tasks.await(
-					Wearable.getNodeClient(appContext).getConnectedNodes()
-				);
-				if (nodes != null) {
-					for (Node node : nodes) {
-						if (node.getId() != null) {
-							switch (component) {
-								case TYPE_NOTIFICATION:
-									Wearable.getMessageClient(appContext)
-										.sendMessage(node.getId(), "/cancel-notification", null);
-									break;
-								case TYPE_ACTIVITY:
-									Wearable.getMessageClient(appContext)
-										.sendMessage(node.getId(), "/cancel-activity", null);
-									break;
-								default:
-									break;
-							}
-						}
-					}
-				}
-			} catch (ExecutionException e) {
-				final String message = e.getMessage();
-				if (message != null && message.contains("Wearable.API is not available on this device")) {
-					logger.debug("cancelOnWearable: ExecutionException while trying to connect to wearable: {}", message);
-				} else {
-					logger.info("cancelOnWearable: ExecutionException while trying to connect to wearable: {}", message);
-				}
-			} catch (InterruptedException e) {
-				logger.info("cancelOnWearable: Interrupted while waiting for wearable client");
-				// Restore interrupted state...
-				Thread.currentThread().interrupt();
-			}
-		});
-	}
-
 	/**
 	 * Return the current call duration in seconds.
 	 *
@@ -1519,8 +1442,8 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 		}
 
 		// WEARABLE
-		if (ConfigUtils.isPlayServicesInstalled(appContext)){
-			this.showWearableNotification(contact, callId, avatar);
+		if (PushService.playServicesInstalled(appContext)){
+			wearableHandler.showWearableNotification(contact, callId, avatar);
 		}
 	}
 
@@ -1591,37 +1514,6 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 		}
 	}
 
-	/*
-	 *  Send information to the companion app on the wearable device
-	 */
-	@WorkerThread
-	private void showWearableNotification(
-		@NonNull ContactModel contact,
-		long callId,
-		@Nullable Bitmap avatar
-	) {
-		final DataClient dataClient = Wearable.getDataClient(appContext);
-
-		// Add data to the request
-		final PutDataMapRequest putDataMapRequest = PutDataMapRequest.create("/incoming-call");
-		putDataMapRequest.getDataMap().putLong(EXTRA_CALL_ID, callId);
-		putDataMapRequest.getDataMap().putString(EXTRA_CONTACT_IDENTITY, contact.getIdentity());
-		logger.debug("sending the following contactIdentity from VoipState to wearable " + contact.getIdentity());
-		putDataMapRequest.getDataMap().putString("CONTACT_NAME", NameUtil.getDisplayNameOrNickname(contact, true));
-		putDataMapRequest.getDataMap().putLong("CALL_TIME", System.currentTimeMillis());
-		if (avatar != null) {
-			final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
-			avatar.compress(Bitmap.CompressFormat.PNG, 100, buffer);
-			putDataMapRequest.getDataMap().putByteArray("CONTACT_AVATAR", buffer.toByteArray());
-		}
-
-		final PutDataRequest request = putDataMapRequest.asPutDataRequest();
-		request.setUrgent();
-
-		dataClient.addListener(wearableListener);
-		dataClient.putDataItem(request);
-	}
-
 	private PendingIntent createLaunchPendingIntent(
 		@NonNull String identity,
 		@Nullable VoipCallOfferMessage msg

+ 15 - 3
app/src/main/java/ch/threema/app/webclient/converter/ClientInfo.java

@@ -33,10 +33,12 @@ import androidx.annotation.Nullable;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.managers.ServiceManager;
+import ch.threema.app.push.PushService;
 import ch.threema.app.services.PreferenceService;
 import ch.threema.app.services.license.LicenseService;
 import ch.threema.app.utils.AppRestrictionUtil;
 import ch.threema.app.utils.ConfigUtils;
+import ch.threema.app.utils.PushUtil;
 import ch.threema.app.webclient.exceptions.ConversionException;
 
 @AnyThread
@@ -80,8 +82,10 @@ public class ClientInfo extends Converter {
 	private final static String DISABLE_CALLS = "disableCalls";
 	private final static String READONLY_PROFILE = "readonlyProfile";
 
-	public static MsgpackObjectBuilder convert(@NonNull Context appContext,
-	                                           @Nullable String pushToken) throws ConversionException {
+	public static MsgpackObjectBuilder convert(
+		@NonNull Context appContext,
+	    @Nullable String pushToken
+	) throws ConversionException {
 		// Services
 		final ServiceManager serviceManager = ThreemaApplication.getServiceManager();
 		if (serviceManager == null) {
@@ -104,7 +108,15 @@ public class ClientInfo extends Converter {
 		data.put(OS, "android");
 		data.put(OS_VERSION, Build.VERSION.RELEASE);
 		data.put(APP_VERSION, ConfigUtils.getFullAppVersion(appContext));
-		data.maybePut(PUSH_TOKEN, pushToken);
+		if (pushToken != null) {
+			// To be able to differentiate between HMS and FCM push tokens without any
+			// protocol changes, we'll prefix the push token with "hms;".
+			if (PushService.hmsServicesInstalled(appContext)) {
+				data.put(PUSH_TOKEN, "hms;" + pushToken);
+			} else {
+				data.put(PUSH_TOKEN, pushToken);
+			}
+		}
 
 		// Work stuff
 		if (ConfigUtils.isWorkBuild()) {

+ 4 - 1
app/src/main/java/ch/threema/app/webclient/converter/MessageState.java

@@ -22,7 +22,6 @@
 package ch.threema.app.webclient.converter;
 
 import androidx.annotation.AnyThread;
-
 import ch.threema.app.webclient.exceptions.ConversionException;
 
 @AnyThread
@@ -35,6 +34,7 @@ public class MessageState extends Converter {
 	public static final String USERDEC = "user-dec";
 	public static final String PENDING = "pending";
 	public static final String SENDING = "sending";
+	public static final String CONSUMED = "consumed";
 
 	public static String convert(ch.threema.storage.models.MessageState state) throws ConversionException {
 		try {
@@ -56,6 +56,9 @@ public class MessageState extends Converter {
 					return MessageState.PENDING;
 				case SENDING:
 					return MessageState.SENDING;
+				case CONSUMED:
+					// TODO(WEBC-432): change to MessageState.CONSUMED as soon as Threema Web supports it
+					return MessageState.READ;
 				default:
 					throw new ConversionException("Unknown message state: " + state);
 			}

+ 62 - 52
app/src/main/java/ch/threema/client/APIConnector.java

@@ -49,6 +49,7 @@ import java.util.Arrays;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 
 import javax.crypto.Mac;
@@ -124,44 +125,16 @@ public class APIConnector {
 		this(ipv6, null, isWork, sandbox, sslSocketFactoryFactory);
 	}
 
-	/**
-	 * Create a new identity and store it in the given identity store.
-	 *
-	 * @param identityStore the store for the new identity
-	 * @throws Exception
-	 */
-	public void createIdentity(IdentityStoreInterface identityStore) throws Exception {
-		createIdentity(identityStore, null);
-	}
-
-	/**
-	 * Create a new identity and store it in the given identity store.
-	 *
-	 * @param identityStore the store for the new identity
-	 * @param seed          additional random data to be used for key generation
-	 * @throws Exception
-	 */
-	public void createIdentity(IdentityStoreInterface identityStore, byte[] seed) throws Exception {
-		createIdentity(identityStore, seed, null, null, null, null, null, null);
-	}
-
 	/**
 	 * Create a new identity and store it in the given identity store.
 	 *
 	 * @param identityStore   the store for the new identity
 	 * @param seed            additional random data to be used for key generation
-	 * @param lvlResponseData response data from Google LVL
-	 * @param lvlSignature    signature from Google LVL
-	 * @param deviceId        unique device ID
-	 * @param licenseKey      license key for direct distribution (or null)
-	 * @param licenseUsername license username for work (or null)
-	 * @param licensePassword license password for work (or null)
+	 * @param requestData    licensing requestData based on build flavor (hms, google or serial)
 	 * @throws Exception
 	 */
 	public void createIdentity(IdentityStoreInterface identityStore, byte[] seed,
-							   String lvlResponseData, String lvlSignature,
-							   String deviceId, String licenseKey,
-							   String licenseUsername, String licensePassword) throws Exception {
+							   @NonNull CreateIdentityRequestDataInterface requestData) throws Exception {
 		String url = serverUrl + "identity/create";
 
 		/* generate new key pair and store */
@@ -199,32 +172,11 @@ public class APIConnector {
 		NaCl nacl = new NaCl(privateKey, tokenRespKeyPub);
 		byte[] clientResponse = nacl.encrypt(token, nonceStr.getBytes());
 
-		JSONObject p2Body = new JSONObject();
+		JSONObject p2Body = requestData.createIdentityRequestDataJSON();
 		p2Body.put("publicKey", Base64.encodeBytes(publicKey));
 		p2Body.put("token", tokenString);
 		p2Body.put("response", Base64.encodeBytes(clientResponse));
 
-		if (lvlResponseData != null && lvlSignature != null) {
-			p2Body.put("lvlResponseData", lvlResponseData);
-			p2Body.put("lvlSignature", lvlSignature);
-		}
-
-		if (deviceId != null) {
-			p2Body.put("deviceId", deviceId);
-		}
-
-		if (licenseKey != null) {
-			p2Body.put("licenseKey", licenseKey);
-		}
-
-		if (licenseUsername != null) {
-			p2Body.put("licenseUsername", licenseUsername);
-		}
-
-		if (licensePassword != null) {
-			p2Body.put("licensePassword", licensePassword);
-		}
-
 		String p2ResultString = doPost(url, p2Body.toString());
 		JSONObject p2Result = new JSONObject(p2ResultString);
 
@@ -1133,6 +1085,64 @@ public class APIConnector {
 		}
 		return workData;
 	}
+
+	/**
+	 * Fetch work contacts from work api
+	 *
+	 * @param username (threema work license username)
+	 * @param password (threema work license password)
+	 * @param identities (list of threema id to check)
+	 * @return list of valid threema work contacts - empty list if there are no matching contacts in this package
+	 * @throws IOException
+	 * @throws JSONException
+	 */
+	@NonNull
+	public List<WorkContact> fetchWorkContacts(@NonNull String username,
+	                                           @NonNull String password,
+	                                           @NonNull String[] identities) throws Exception {
+
+		List<WorkContact> contactsList = new ArrayList<>();
+		JSONObject request = new JSONObject();
+		request.put("username", username);
+		request.put("password", password);
+
+		JSONArray identityArray = new JSONArray();
+		for (String identity : identities) {
+			identityArray.put(identity);
+		}
+		request.put("contacts", identityArray);
+
+		String data = doPost(
+			this.workServerUrl + "identities",
+			request.toString());
+
+		if (data == null || data.length() == 0) {
+			return contactsList;
+		}
+
+		JSONObject jsonResponse = new JSONObject(data);
+
+		if (jsonResponse.has("contacts")) {
+			JSONArray contacts = jsonResponse.getJSONArray("contacts");
+
+			for (int n = 0; n < contacts.length(); n++) {
+				JSONObject contact = contacts.getJSONObject(n);
+
+				//validate fields
+				if (contact.has("id") && contact.has("pk")) {
+					contactsList.add(new WorkContact(
+						contact.getString("id"),
+						Base64.decode(contact.getString("pk")),
+						contact.has("first") ? contact.getString("first") : null,
+						contact.has("last") ? contact.getString("last") : null
+					));
+				}
+			}
+		}
+
+		return contactsList;
+	}
+
 	/**
 	 * Search the threema work directory without categories
 	 *

+ 8 - 0
app/src/main/java/ch/threema/client/CreateIdentityRequestDataInterface.java

@@ -0,0 +1,8 @@
+package ch.threema.client;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public interface CreateIdentityRequestDataInterface {
+	JSONObject createIdentityRequestDataJSON() throws JSONException;
+}

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

@@ -119,6 +119,7 @@ public class ProtocolDefines {
 	public static final int DELIVERYRECEIPT_MSGREAD = 0x02;
 	public static final int DELIVERYRECEIPT_MSGUSERACK = 0x03;
 	public static final int DELIVERYRECEIPT_MSGUSERDEC = 0x04;
+	public static final int DELIVERYRECEIPT_MSGCONSUMED = 0x05;
 
 	/* payload types */
 	public static final int PLTYPE_ECHO_REQUEST = 0x00;
@@ -137,6 +138,7 @@ public class ProtocolDefines {
 	/* push token types */
 	public static final int PUSHTOKEN_TYPE_NONE = 0x00;
 	public static final int PUSHTOKEN_TYPE_GCM = 0x11;
+	public static final int PUSHTOKEN_TYPE_HMS = 0x13;
 
 	/* nonces */
 	public static final byte[] IMAGE_NONCE = new byte[]{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01};

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

@@ -39,7 +39,7 @@ public class ProtocolStrings {
 	public static final String WORK_SERVER_URL_SANDBOX_IPV6 = "";
 
 	public static final String BLOB_UPLOAD_URL = "https://blobp-upload.threema.ch/upload";
-	public static final String BLOB_UPLOAD_URL_IPV6 = "https://blobp-upload.threema.ch/upload";
+	public static final String BLOB_UPLOAD_URL_IPV6 = "https://ds-blobp-upload.threema.ch/upload";
 	public static final String BLOB_URL_PATTERN = "https://blobp-%s.threema.ch/%s";
 	public static final String BLOB_URL_PATTERN_IPV6 = "https://ds-blobp-%s.threema.ch/%s";
 	public static final String BLOB_DONE_PATTERN = "https://blobp-%s.threema.ch/%s/done";

+ 2 - 1
app/src/main/java/ch/threema/storage/models/MessageState.java

@@ -30,5 +30,6 @@ public enum MessageState {
 	USERDEC,
 	TRANSCODING,
 	PENDING,
-	SENDING
+	SENDING,
+	CONSUMED
 }

+ 6 - 5
app/src/main/java/ch/threema/storage/models/TagModel.java

@@ -22,6 +22,7 @@
 package ch.threema.storage.models;
 
 import androidx.annotation.ColorInt;
+import androidx.annotation.StringRes;
 
 /**
  * Important: not stored in the db now
@@ -31,13 +32,13 @@ public class TagModel {
 	private final String tag;
 	@ColorInt private final int primaryColor;
 	@ColorInt private final int accentColor;
-	private final String name;
+	@StringRes final int nameRes;
 
-	public TagModel(String tag, @ColorInt int primaryColor, @ColorInt int accentColor, String name) {
+	public TagModel(String tag, @ColorInt int primaryColor, @ColorInt int accentColor, @StringRes int nameRes) {
 		this.tag = tag;
 		this.primaryColor = primaryColor;
 		this.accentColor = accentColor;
-		this.name = name;
+		this.nameRes = nameRes;
 	}
 
 	public String getTag() {
@@ -52,7 +53,7 @@ public class TagModel {
 		return this.accentColor;
 	}
 
-	public String getName() {
-		return this.name;
+	public @StringRes int getNameRes() {
+		return this.nameRes;
 	}
 }

+ 92 - 0
app/src/main/java/com/DrmSDK/Constants.java

@@ -0,0 +1,92 @@
+/*
+ * Copyright 2020. Huawei Technologies Co., Ltd. All rights reserved.
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+
+ *http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.DrmSDK;
+
+/**
+ * 公共常量定义类
+ * Common constant definition class
+ *
+ * @since 2020/07/01
+ */
+public class Constants {
+    /**
+     * Bundle key: 拉起Activity.
+     * Bundle key: Pull up the activity
+     */
+    public static final String KEY_EXTRA_ACTION = "drm_key_extra_actiion";
+    /**
+     * Bundle key: 拉起Activity的包名
+     * Bundle key: name of the package for starting the activity.
+     */
+    public static final String KEY_EXTRA_PACKAGE = "drm_key_extra_package";
+    /**
+     * Bundle key: Dialog类型
+     * Bundle key: Dialog type
+     */
+    public static final String KEY_EXTRA_DIALOG = "drm_key_extra_dialog";
+    /**
+     * Bundle key: 错误码类型
+     * Bundle key: error code type.
+     */
+    public static final String KEY_EXTRA_CODE = "drm_key_extra_code";
+    /**
+     * Bundle key: 其他信息
+     * Bundle key: other information
+     */
+    public static final String KEY_EXTRA_EXTRA = "drm_key_extra_extra";
+    /**
+     * Bundle key: 向外部activity传递的通用参数
+     * Bundle key: common parameter transferred to an external activity.
+     */
+    public static final String KEY_JSON_EXTRA = "json_extra";
+    /**
+     * 拉起详情页的action
+     * Action for starting the details page
+     */
+    public static final String DOOR_TO_ALLY_OF_DETAIL = "com.huawei.appmarket.intent.action.AppDetail";
+    /**
+     * Activity返回码:同意用户使用协议
+     * Activity return code: agreeing to the user agreement
+     */
+    public static final int RESULT_CODE_AGREEMENT_AGREED = 1001;
+    /**
+     * Activity返回码:不同意用户使用协议
+     * Activity return code: The user agreement is not approved.
+     */
+    public static final int RESULT_CODE_AGREEMENT_DECLINED = 1002;
+    /**
+     * Activity返回码:账号登陆成功
+     * Activity return code: The login is successful.
+     */
+    public static final int RESULT_CODE_LOGIN_SUCCESS = 10001;
+    /**
+     * Activity返回码:账号登陆失败
+     * Activity return code: account login failure
+     */
+    public static final int RESULT_CODE_LOGIN_FAILED = 10002;
+
+    /**
+     * 加入会员成功
+     * Member added successfully.
+     */
+    public static final int RESULT_CODE_JOIN_MEMBER_FAILED = 20001;
+    /**
+     * 加入会员失败
+     * Failed to join the member.
+     */
+    public static final int RESULT_CODE_JOIN_MEMBER_SUCCESS = 20002;
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов