Threema пре 4 година
родитељ
комит
dafa3883c9
100 измењених фајлова са 732 додато и 912 уклоњено
  1. 1 1
      README.md
  2. 9 3
      app/assets/license.html
  3. 20 20
      app/build.gradle
  4. 0 179
      app/src/google_services_based/java/ch/threema/app/wearable/WearableHandler.java
  5. 18 0
      app/src/hms/AndroidManifest.xml
  6. 15 15
      app/src/hms/agconnect-services.json
  7. 11 14
      app/src/hms_services_based/java/ch/threema/app/push/PushRegistrationWorker.java
  8. 16 0
      app/src/hms_work/AndroidManifest.xml
  9. 15 15
      app/src/hms_work/agconnect-services.json
  10. 16 35
      app/src/main/java/ch/threema/app/ThreemaApplication.java
  11. 5 1
      app/src/main/java/ch/threema/app/activities/AddContactActivity.java
  12. 2 2
      app/src/main/java/ch/threema/app/activities/BackupAdminActivity.java
  13. 2 7
      app/src/main/java/ch/threema/app/activities/ContactDetailActivity.java
  14. 2 2
      app/src/main/java/ch/threema/app/activities/DirectoryActivity.java
  15. 2 3
      app/src/main/java/ch/threema/app/activities/DisableBatteryOptimizationsActivity.java
  16. 2 8
      app/src/main/java/ch/threema/app/activities/GroupDetailActivity.java
  17. 2 3
      app/src/main/java/ch/threema/app/activities/MapActivity.java
  18. 15 10
      app/src/main/java/ch/threema/app/activities/RecipientListBaseActivity.java
  19. 2 2
      app/src/main/java/ch/threema/app/activities/SendMediaActivity.java
  20. 10 0
      app/src/main/java/ch/threema/app/activities/StorageManagementActivity.java
  21. 3 3
      app/src/main/java/ch/threema/app/activities/TextChatBubbleActivity.java
  22. 2 2
      app/src/main/java/ch/threema/app/activities/ThreemaActivity.java
  23. 2 2
      app/src/main/java/ch/threema/app/activities/ThreemaToolbarActivity.java
  24. 2 1
      app/src/main/java/ch/threema/app/activities/wizard/WizardIntroActivity.java
  25. 1 1
      app/src/main/java/ch/threema/app/adapters/MediaGalleryAdapter.java
  26. 2 2
      app/src/main/java/ch/threema/app/adapters/decorators/AudioChatAdapterDecorator.java
  27. 1 1
      app/src/main/java/ch/threema/app/adapters/decorators/FileChatAdapterDecorator.java
  28. 1 1
      app/src/main/java/ch/threema/app/adapters/decorators/ImageChatAdapterDecorator.java
  29. 1 1
      app/src/main/java/ch/threema/app/adapters/decorators/TextChatAdapterDecorator.java
  30. 8 17
      app/src/main/java/ch/threema/app/adapters/decorators/VideoChatAdapterDecorator.java
  31. 4 4
      app/src/main/java/ch/threema/app/asynctasks/DeleteIdentityAsyncTask.java
  32. 2 2
      app/src/main/java/ch/threema/app/backuprestore/csv/BackupRestoreDataServiceImpl.java
  33. 2 2
      app/src/main/java/ch/threema/app/backuprestore/csv/RestoreService.java
  34. 14 70
      app/src/main/java/ch/threema/app/fragments/ComposeMessageFragment.java
  35. 52 21
      app/src/main/java/ch/threema/app/fragments/MessageSectionFragment.java
  36. 6 2
      app/src/main/java/ch/threema/app/fragments/wizard/WizardFragment4.java
  37. 2 13
      app/src/main/java/ch/threema/app/jobs/ShareTargetShortcutUpdateJobService.java
  38. 0 15
      app/src/main/java/ch/threema/app/managers/ServiceManager.java
  39. 2 1
      app/src/main/java/ch/threema/app/messagereceiver/ContactMessageReceiver.java
  40. 2 2
      app/src/main/java/ch/threema/app/messagereceiver/GroupMessageReceiver.java
  41. 11 7
      app/src/main/java/ch/threema/app/preference/SettingsAboutFragment.java
  42. 2 10
      app/src/main/java/ch/threema/app/preference/SettingsPrivacyFragment.java
  43. 2 1
      app/src/main/java/ch/threema/app/processors/MessageProcessor.java
  44. 1 1
      app/src/main/java/ch/threema/app/routines/SynchronizeContactsRoutine.java
  45. 2 2
      app/src/main/java/ch/threema/app/services/ActivityService.java
  46. 2 2
      app/src/main/java/ch/threema/app/services/AppRestrictionService.java
  47. 2 2
      app/src/main/java/ch/threema/app/services/AvatarCacheServiceImpl.java
  48. 6 2
      app/src/main/java/ch/threema/app/services/ContactServiceImpl.java
  49. 2 2
      app/src/main/java/ch/threema/app/services/ConversationServiceImpl.java
  50. 5 0
      app/src/main/java/ch/threema/app/services/DistributionListServiceImpl.java
  51. 1 2
      app/src/main/java/ch/threema/app/services/DownloadService.java
  52. 14 16
      app/src/main/java/ch/threema/app/services/DownloadServiceImpl.java
  53. 7 3
      app/src/main/java/ch/threema/app/services/FileServiceImpl.java
  54. 6 6
      app/src/main/java/ch/threema/app/services/GroupMessagingServiceImpl.java
  55. 8 1
      app/src/main/java/ch/threema/app/services/GroupServiceImpl.java
  56. 2 1
      app/src/main/java/ch/threema/app/services/LifetimeServiceImpl.java
  57. 60 62
      app/src/main/java/ch/threema/app/services/MessageServiceImpl.java
  58. 6 1
      app/src/main/java/ch/threema/app/services/ServerAddressProviderServiceImpl.java
  59. 0 46
      app/src/main/java/ch/threema/app/services/ShortcutService.java
  60. 1 1
      app/src/main/java/ch/threema/app/services/messageplayer/AudioMessagePlayer.java
  61. 1 3
      app/src/main/java/ch/threema/app/utils/AndroidContactUtil.java
  62. 10 9
      app/src/main/java/ch/threema/app/utils/BitmapUtil.java
  63. 27 0
      app/src/main/java/ch/threema/app/utils/ConfigUtils.java
  64. 7 2
      app/src/main/java/ch/threema/app/utils/ConversationNotificationUtil.java
  65. 2 2
      app/src/main/java/ch/threema/app/utils/MessageUtil.java
  66. 147 88
      app/src/main/java/ch/threema/app/utils/ShortcutUtil.java
  67. 4 3
      app/src/main/java/ch/threema/app/utils/StringConversionUtil.java
  68. 2 2
      app/src/main/java/ch/threema/app/voip/AudioSelectorButton.java
  69. 2 2
      app/src/main/java/ch/threema/app/voip/CallAnswerIndicatorLayout.java
  70. 3 2
      app/src/main/java/ch/threema/app/voip/CallState.java
  71. 4 4
      app/src/main/java/ch/threema/app/voip/PeerConnectionClient.java
  72. 2 2
      app/src/main/java/ch/threema/app/voip/VoipAudioManager.java
  73. 2 2
      app/src/main/java/ch/threema/app/voip/VoipBluetoothManager.java
  74. 2 2
      app/src/main/java/ch/threema/app/voip/activities/CallActionIntentActivity.java
  75. 13 12
      app/src/main/java/ch/threema/app/voip/activities/CallActivity.java
  76. 2 2
      app/src/main/java/ch/threema/app/voip/activities/WebRTCDebugActivity.java
  77. 2 2
      app/src/main/java/ch/threema/app/voip/receivers/IncomingMobileCallReceiver.java
  78. 2 2
      app/src/main/java/ch/threema/app/voip/receivers/VoipMediaButtonReceiver.java
  79. 2 2
      app/src/main/java/ch/threema/app/voip/services/CallRejectService.java
  80. 3 4
      app/src/main/java/ch/threema/app/voip/services/VideoContext.java
  81. 2 21
      app/src/main/java/ch/threema/app/voip/services/VoipCallService.java
  82. 6 32
      app/src/main/java/ch/threema/app/voip/services/VoipStateService.java
  83. 2 2
      app/src/main/java/ch/threema/app/voip/util/VideoCapturerUtil.java
  84. 2 2
      app/src/main/java/ch/threema/app/voip/util/VoipUtil.java
  85. 2 2
      app/src/main/java/ch/threema/app/voip/util/VoipVideoParams.java
  86. 10 2
      app/src/main/java/ch/threema/app/webclient/activities/SessionsActivity.java
  87. 4 5
      app/src/main/java/ch/threema/app/webclient/activities/SessionsIntroActivity.java
  88. 2 2
      app/src/main/java/ch/threema/app/webclient/services/WakeLockServiceImpl.java
  89. 3 3
      app/src/main/java/ch/threema/app/webclient/services/instance/state/SessionConnectionContext.java
  90. 2 2
      app/src/main/java/ch/threema/app/webrtc/FlowControlledDataChannel.java
  91. 5 6
      app/src/main/java/ch/threema/app/webrtc/UnboundedFlowControlledDataChannel.java
  92. 3 3
      app/src/main/java/ch/threema/app/workers/IdentityStatesWorker.java
  93. 4 1
      app/src/main/java/ch/threema/logging/LoggerManager.java
  94. 2 1
      app/src/main/java/ch/threema/logging/backend/DebugLogFileBackend.java
  95. 1 1
      app/src/main/java/ch/threema/logging/backend/LogcatBackend.java
  96. 0 6
      app/src/main/java/ch/threema/storage/models/data/media/AudioDataModel.java
  97. 20 10
      app/src/main/java/ch/threema/storage/models/data/media/FileDataModel.java
  98. 5 9
      app/src/main/java/ch/threema/storage/models/data/media/VideoDataModel.java
  99. 2 2
      app/src/main/res/drawable-v24/ic_thumbscroller.xml
  100. 2 2
      app/src/main/res/drawable/ic_thumbscroller.xml

+ 1 - 1
README.md

@@ -221,7 +221,7 @@ language, please sign up at <https://threema.oneskyapp.com/collaboration/>.
 
 Threema for Android is licensed under the GNU Affero General Public License v3.
 
-    Copyright (c) 2013-2021 Threema GmbH
+    Copyright (c) 2013-2022 Threema GmbH
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU Affero General Public License, version 3,

+ 9 - 3
app/assets/license.html

@@ -43,7 +43,7 @@
 
 <body>
 
-<p class="maincopyright">Copyright © 2021 Threema GmbH.<br/>
+<p class="maincopyright">Copyright © 2022 Threema GmbH.<br/>
     All rights reserved.</p>
 
 <p>This product contains artwork and code from the following rights holders:</p>
@@ -227,9 +227,15 @@ POSSIBILITY OF SUCH DAMAGE.</p>
 <p>Licensed under the Apache License, version 2.0 (copy below).</p>
 
 
-<h2>Mapbox Maps SDK for Android</h2>
+<h2>MapLibre GL Native</h2>
 
-<p>Copyright 2014-2020 Mapbox.</p>
+<p>Copyright (c) 2021 MapLibre contributors</p>
+
+<p>Copyright (c) 2018-2021 MapTiler.com</p>
+
+<p>Copyright 2014-2020 Mapbox</p>
+
+<p>Licensed under the BSD 2-Clause License.</p>
 
 <p>Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:</p>
 

+ 20 - 20
app/build.gradle

@@ -20,7 +20,7 @@ if (getGradle().getStartParameter().getTaskRequests().toString().contains("Hms")
 }
 
 // version codes
-def app_version = "4.63"
+def app_version = "4.64"
 def beta_suffix = "" // with leading dash
 
 /**
@@ -99,10 +99,10 @@ android {
         vectorDrawables.useSupportLibrary = true
         applicationId "ch.threema.app"
         testApplicationId 'ch.threema.app.test'
-        versionCode 712
+        versionCode 715
         versionName "${app_version}${beta_suffix}"
         resValue "string", "app_name", "Threema"
-        // package name used for sync adapter
+        // package name used for sync adapter - needs to match mime types below
         resValue "string", "package_name", applicationId
         resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.profile"
         resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.call"
@@ -130,6 +130,7 @@ android {
         buildConfigField "String", "BLOB_SERVER_UPLOAD_IPV6_URL", "\"https://ds-blobp-upload.threema.ch/upload\""
         buildConfigField "String", "AVATAR_FETCH_URL", "\"https://avatar.threema.ch/\""
         buildConfigField "String", "SAFE_SERVER_URL", "\"https://safe-%h.threema.ch/\""
+        buildConfigField "String", "WEB_SERVER_URL", "\"https://web.threema.ch/\""
         buildConfigField "String", "ONPREM_ID_PREFIX", "\"O\""
 
         buildConfigField "String[]", "ONPREM_CONFIG_TRUSTED_PUBLIC_KEYS", "null"
@@ -189,7 +190,7 @@ android {
             applicationId "ch.threema.app.work"
             testApplicationId 'ch.threema.app.work.test'
             resValue "string", "app_name", "Threema Work"
-
+            resValue "string", "package_name", applicationId
             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"
             buildConfigField "String", "CHAT_SERVER_PREFIX", "\"w-\""
@@ -211,7 +212,9 @@ android {
             applicationId "ch.threema.app.sandbox"
             testApplicationId 'ch.threema.app.sandbox.test'
             resValue "string", "app_name", "Threema Sandbox"
-
+            resValue "string", "package_name", applicationId
+            resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.sandbox.profile"
+            resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.sandbox.call"
             buildConfigField "String", "MEDIA_PATH", "\"ThreemaSandbox\""
             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 }"
@@ -225,9 +228,9 @@ android {
             applicationId "ch.threema.app.sandbox.work"
             testApplicationId 'ch.threema.app.sandbox.work.test'
             resValue "string", "app_name", "Threema Sandbox Work"
-
-            resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.work.profile"
-            resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.work.call"
+            resValue "string", "package_name", applicationId
+            resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.sandbox.work.profile"
+            resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.sandbox.work.call"
             buildConfigField "String", "CHAT_SERVER_PREFIX", "\"w-\""
             buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"ds.w-\""
             buildConfigField "String", "CHAT_SERVER_SUFFIX", "\".0.test.threema.ch\""
@@ -255,7 +258,7 @@ android {
             applicationId "ch.threema.app.onprem"
             testApplicationId 'ch.threema.app.onprem.test'
             resValue "string", "app_name", "Threema OnPrem"
-
+            resValue "string", "package_name", applicationId
             resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.onprem.profile"
             resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.onprem.call"
             buildConfigField "int", "MAX_GROUP_SIZE", "256"
@@ -292,9 +295,9 @@ android {
             applicationId "ch.threema.app.red"
             testApplicationId 'ch.threema.app.red.test'
             resValue "string", "app_name", "Threema Red"
-
-            resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.redwork.profile"
-            resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.redwork.call"
+            resValue "string", "package_name", applicationId
+            resValue "string", "contacts_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.red.profile"
+            resValue "string", "call_mime_type", "vnd.android.cursor.item/vnd.ch.threema.app.red.call"
 
             buildConfigField "String", "CHAT_SERVER_PREFIX", "\"w-\""
             buildConfigField "String", "CHAT_SERVER_IPV6_PREFIX", "\"ds.w-\""
@@ -327,7 +330,7 @@ android {
             applicationId "ch.threema.app.work.hms"
             testApplicationId 'ch.threema.app.work.test.hms'
             resValue "string", "app_name", "Threema Work"
-
+            resValue "string", "package_name", "ch.threema.app.work"
             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"
             buildConfigField "String", "CHAT_SERVER_PREFIX", "\"w-\""
@@ -617,13 +620,13 @@ dependencies {
 
     implementation project(':domain')
 
-    implementation 'net.zetetic:android-database-sqlcipher:4.4.3'
+    implementation 'net.zetetic:android-database-sqlcipher:4.5.0'
 
     implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
     implementation 'net.sf.opencsv:opencsv:2.3'
-    implementation 'net.lingala.zip4j:zip4j:2.9.0'
+    implementation 'net.lingala.zip4j:zip4j:2.9.1'
     implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.2'
-    implementation 'com.mapbox.mapboxsdk:mapbox-android-sdk:9.2.1'
+    implementation 'org.maplibre.gl:android-sdk:9.5.2'
     // commons-io >2.6 requires android 8
     implementation 'commons-io:commons-io:2.6'
     implementation "org.slf4j:slf4j-api:$slf4j_version"
@@ -668,7 +671,7 @@ dependencies {
     implementation 'com.google.android.exoplayer:exoplayer-core:2.15.1'
     implementation 'com.google.android.exoplayer:exoplayer-ui:2.15.1'
     implementation 'com.google.zxing:core:3.3.3' // zxing 3.4 crashes on kitkat
-    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.37'
+    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.41' // make sure to update this in domain's build.gradle as well
 
     // webclient dependencies
     implementation 'org.msgpack:msgpack-core:0.8.22!!'
@@ -747,9 +750,6 @@ dependencies {
         // 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,

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

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

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

@@ -5,6 +5,24 @@
           android:testOnly="false">
 	<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
 
+	<queries>
+		<package android:name="com.hisilicon.android.hiRMService" />
+		<intent>
+			<action android:name="com.apptouch.intent.action.update_hms" />
+		</intent>
+		<intent>
+			<action android:name="com.huawei.appmarket.intent.action.AppDetail" />
+		</intent>
+
+		<intent>
+			<action android:name="com.huawei.hms.core.aidlservice" />
+		</intent>
+
+		<intent>
+			<action android:name="com.huawei.hms.core" />
+		</intent>
+	</queries>
+
 	<application tools:ignore="GoogleAppIndexingWarning">
 
 		<activity

+ 15 - 15
app/src/hms/agconnect-services.json

@@ -1,19 +1,19 @@
 {
 	"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"
+		"backurl":"connect-dre.hispace.hicloud.com",
+		"url":"connect-dre.dbankcloud.cn",
+		"websocketbackurl":"connect-ws-dre.hispace.dbankcloud.com",
+		"websocketurl":"connect-ws-dre.hispace.dbankcloud.cn"
 	},
 	"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"
+		"CN":"connect-drcn.dbankcloud.cn",
+		"CN_back":"connect-drcn.hispace.hicloud.com",
+		"DE":"connect-dre.dbankcloud.cn",
+		"DE_back":"connect-dre.hispace.hicloud.com",
+		"RU":"connect-drru.dbankcloud.cn",
+		"RU_back":"connect-drru.hispace.hicloud.com",
+		"SG":"connect-dra.dbankcloud.cn",
+		"SG_back":"connect-dra.hispace.hicloud.com"
 	},
 	"client":{
 		"cp_id":"5190041000024384032",
@@ -30,10 +30,10 @@
 		{
 			"package_name":"ch.threema.app.hms",
 			"app_id":"103713829"
-		},
+			},
 		{
-			"package_name":"ch.threema.app.work.hms",
-			"app_id":"103858571"
+				"package_name":"ch.threema.app.work.hms",
+				"app_id":"103858571"
 		}
 	]
 }

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

@@ -25,7 +25,6 @@ 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;
@@ -35,7 +34,6 @@ 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.domain.protocol.csp.ProtocolDefines;
 
 public class PushRegistrationWorker extends Worker {
@@ -64,8 +62,8 @@ public class PushRegistrationWorker extends Worker {
 		final boolean withCallback = workerFlags.getBoolean(PushUtil.EXTRA_WITH_CALLBACK, false);
 		logger.debug("doWork HMS token registration clear {} withCallback {}", clearToken, withCallback);
 
+		String error = null;
 		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);
@@ -74,30 +72,29 @@ public class PushRegistrationWorker extends Worker {
 				HmsInstanceId.getInstance(appContext).deleteToken(appId, TOKEN_SCOPE);
 				PushUtil.sendTokenToServer(appContext,"", ProtocolDefines.PUSHTOKEN_TYPE_NONE);
 				logger.info("HMS token successfully deleted");
-			} catch (ApiException | ThreemaException e) {
+			} catch (Exception 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 appId = AGConnectServicesConfig.fromContext(appContext).getString(APP_ID_CONFIG_FIELD);
+
 				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) {
+			} catch (Exception e) {
 				logger.error("Exception", e);
 				error = e.getMessage();
 			}
-			if (withCallback) {
-				PushUtil.signalRegistrationFinished(error, clearToken);
-			}
+
+		}
+
+		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();
 	}

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

@@ -5,6 +5,22 @@
           android:testOnly="false">
 	<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
 
+	<queries>
+		<package android:name="com.hisilicon.android.hiRMService" />
+		<intent>
+			<action android:name="com.apptouch.intent.action.update_hms" />
+		</intent>
+		<intent>
+			<action android:name="com.huawei.appmarket.intent.action.AppDetail" />
+		</intent>
+		<intent>
+			<action android:name="com.huawei.hms.core.aidlservice" />
+		</intent>
+		<intent>
+			<action android:name="com.huawei.hms.core" />
+		</intent>
+	</queries>
+
 	<application tools:ignore="GoogleAppIndexingWarning">
 
 		<activity

+ 15 - 15
app/src/hms_work/agconnect-services.json

@@ -1,19 +1,19 @@
 {
 	"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"
+		"backurl":"connect-dre.hispace.hicloud.com",
+		"url":"connect-dre.dbankcloud.cn",
+		"websocketbackurl":"connect-ws-dre.hispace.dbankcloud.com",
+		"websocketurl":"connect-ws-dre.hispace.dbankcloud.cn"
 	},
 	"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"
+		"CN":"connect-drcn.dbankcloud.cn",
+		"CN_back":"connect-drcn.hispace.hicloud.com",
+		"DE":"connect-dre.dbankcloud.cn",
+		"DE_back":"connect-dre.hispace.hicloud.com",
+		"RU":"connect-drru.dbankcloud.cn",
+		"RU_back":"connect-drru.hispace.hicloud.com",
+		"SG":"connect-dra.dbankcloud.cn",
+		"SG_back":"connect-dra.hispace.hicloud.com"
 	},
 	"client":{
 		"cp_id":"5190041000024384032",
@@ -30,10 +30,10 @@
 		{
 			"package_name":"ch.threema.app.hms",
 			"app_id":"103713829"
-		},
+			},
 		{
-			"package_name":"ch.threema.app.work.hms",
-			"app_id":"103858571"
+				"package_name":"ch.threema.app.work.hms",
+				"app_id":"103858571"
 		}
 	]
 }

+ 16 - 35
app/src/main/java/ch/threema/app/ThreemaApplication.java

@@ -47,14 +47,11 @@ import android.widget.Toast;
 import com.datatheorem.android.trustkit.TrustKit;
 import com.datatheorem.android.trustkit.reporting.BackgroundReporter;
 import com.google.common.util.concurrent.ListenableFuture;
-import com.mapbox.android.telemetry.TelemetryEnabler;
 import com.mapbox.mapboxsdk.Mapbox;
-import com.mapbox.mapboxsdk.maps.TelemetryDefinition;
 
 import net.sqlcipher.database.SQLiteException;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.io.IOException;
@@ -64,7 +61,6 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
-import java.util.Random;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -142,6 +138,7 @@ import ch.threema.app.utils.LoggingUEH;
 import ch.threema.app.utils.NameUtil;
 import ch.threema.app.utils.PushUtil;
 import ch.threema.app.utils.RuntimeUtil;
+import ch.threema.app.utils.ShortcutUtil;
 import ch.threema.app.utils.StateBitmapUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.WidgetUtil;
@@ -158,6 +155,7 @@ import ch.threema.app.webclient.state.WebClientSessionState;
 import ch.threema.app.workers.IdentityStatesWorker;
 import ch.threema.base.ThreemaException;
 import ch.threema.base.crypto.NonceFactory;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.base.utils.Utils;
 import ch.threema.domain.models.AppVersion;
 import ch.threema.domain.protocol.csp.connection.ConnectionState;
@@ -191,8 +189,7 @@ import static ch.threema.app.services.PreferenceService.Theme_DARK;
 import static ch.threema.app.services.PreferenceService.Theme_LIGHT;
 
 public class ThreemaApplication extends MultiDexApplication implements DefaultLifecycleObserver {
-
-	private static final Logger logger = LoggerFactory.getLogger(ThreemaApplication.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("ThreemaApplication");
 
 	public static final String INTENT_DATA_CONTACT = "identity";
 	public static final String INTENT_DATA_CONTACT_READONLY = "readonly";
@@ -373,7 +370,7 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 			FileUtil.deleteFileOrWarn(
 				messageQueueFile,
 				"message queue file",
-				LoggerFactory.getLogger("LoggingUEH.runOnUncaughtException")
+				LoggingUtil.getThreemaLogger("LoggingUEH.runOnUncaughtException")
 			);
 		});
 		Thread.setDefaultUncaughtExceptionHandler(loggingUEH);
@@ -957,7 +954,7 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 				}
 			}, "scheduleSync").start();
 
-			initMapbox();
+			initMapLibre();
 
 			// setup locale override
 			ConfigUtils.setLocaleOverride(getAppContext(), serviceManager.getPreferenceService());
@@ -971,19 +968,12 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 		}
 	}
 
-	private static void initMapbox() {
-		if (!ConfigUtils.hasNoMapboxSupport()) {
-			// Mapbox Access token
-			Mapbox.getInstance(getAppContext(), String.valueOf(new Random().nextInt()));
-			TelemetryEnabler.updateTelemetryState(TelemetryEnabler.State.DISABLED);
-			TelemetryDefinition telemetryDefinition = Mapbox.getTelemetry();
-			if (telemetryDefinition != null) {
-				telemetryDefinition.setDebugLoggingEnabled(BuildConfig.DEBUG);
-				telemetryDefinition.setUserTelemetryRequestState(false);
-			}
-			logger.debug("*** Mapbox telemetry: " + TelemetryEnabler.retrieveTelemetryStateFromPreferences());
+	private static void initMapLibre() {
+		if (ConfigUtils.hasNoMapboxSupport()) {
+			logger.debug("*** MapLibre disabled due to faulty firmware");
 		} else {
-			logger.debug("*** Mapbox disabled due to faulty firmware");
+			Mapbox.getInstance(getAppContext());
+			logger.debug("*** MapLibre enabled");
 		}
 	}
 
@@ -1114,7 +1104,7 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 						serviceManager.getMessageService().createStatusMessage(
 							serviceManager.getContext().getString(R.string.status_rename_group, groupModel.getName()),
 							messageReceiver);
-						serviceManager.getShortcutService().updatePinnedShortcut(messageReceiver);
+						ShortcutUtil.updatePinnedShortcut(messageReceiver);
 					} catch (ThreemaException e) {
 						logger.error("Exception", e);
 					}
@@ -1126,12 +1116,11 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 				new Thread(() -> {
 					try {
 						MessageReceiver messageReceiver = serviceManager.getGroupService().createReceiver(groupModel);
-
 						serviceManager.getConversationService().refresh(groupModel);
 						serviceManager.getMessageService().createStatusMessage(
 							serviceManager.getContext().getString(R.string.status_group_new_photo),
 							messageReceiver);
-						serviceManager.getShortcutService().updatePinnedShortcut(messageReceiver);
+						ShortcutUtil.updatePinnedShortcut(messageReceiver);
 					} catch (ThreemaException e) {
 						logger.error("Exception", e);
 					}
@@ -1146,7 +1135,6 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 						serviceManager.getBallotService().remove(receiver);
 						serviceManager.getConversationService().removed(groupModel);
 						serviceManager.getNotificationService().cancel(new GroupMessageReceiver(groupModel, null, null, null, null, serviceManager.getApiService()));
-						serviceManager.getShortcutService().deletePinnedShortcut(receiver);
 					} catch (ThreemaException e) {
 						logger.error("Exception", e);
 					}
@@ -1293,7 +1281,6 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 				new Thread(() -> {
 					try {
 						serviceManager.getConversationService().refresh(groupModel);
-						serviceManager.getShortcutService().deletePinnedShortcut(serviceManager.getGroupService().createReceiver(groupModel));
 					} catch (ThreemaException e) {
 						logger.error("Exception", e);
 					}
@@ -1323,7 +1310,7 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 				new Thread(() -> {
 					try {
 						serviceManager.getConversationService().refresh(distributionListModel);
-						serviceManager.getShortcutService().updatePinnedShortcut(serviceManager.getDistributionListService().createReceiver(distributionListModel));
+						ShortcutUtil.updatePinnedShortcut(serviceManager.getDistributionListService().createReceiver(distributionListModel));
 					} catch (ThreemaException e) {
 						logger.error("Exception", e);
 					}
@@ -1336,7 +1323,6 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 				new Thread(() -> {
 					try {
 						serviceManager.getConversationService().removed(distributionListModel);
-						serviceManager.getShortcutService().deletePinnedShortcut(serviceManager.getDistributionListService().createReceiver(distributionListModel));
 					} catch (ThreemaException e) {
 						logger.error("Exception", e);
 					}
@@ -1486,7 +1472,7 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 				new Thread(() -> {
 					try {
 						serviceManager.getConversationService().refresh(modifiedContactModel);
-						serviceManager.getShortcutService().updatePinnedShortcut(serviceManager.getContactService().createReceiver(modifiedContactModel));
+						ShortcutUtil.updatePinnedShortcut(serviceManager.getContactService().createReceiver(modifiedContactModel));
 					} catch (ThreemaException e) {
 						logger.error("Exception", e);
 					}
@@ -1497,7 +1483,7 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 			public void onAvatarChanged(ContactModel contactModel) {
 				new Thread(() -> {
 					try {
-						serviceManager.getShortcutService().updatePinnedShortcut(serviceManager.getContactService().createReceiver(contactModel));
+						ShortcutUtil.updatePinnedShortcut(serviceManager.getContactService().createReceiver(contactModel));
 					} catch (ThreemaException e) {
 						logger.error("Exception", e);
 					}
@@ -1512,11 +1498,6 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 				new Thread(() -> {
 					try {
 						serviceManager.getConversationService().removed(removedContactModel);
-						serviceManager.getShortcutService().deletePinnedShortcut(serviceManager.getContactService().createReceiver(removedContactModel));
-
-						//remove notification from this contact
-
-						//hack. create a receiver to become the notification id
 						serviceManager.getNotificationService().cancel(new ContactMessageReceiver
 							(
 								removedContactModel,
@@ -1871,7 +1852,7 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 		});
 
 		VoipListenerManager.callEventListener.add(new VoipCallEventListener() {
-			private final Logger logger = LoggerFactory.getLogger("VoipCallEventListener");
+			private final Logger logger = LoggingUtil.getThreemaLogger("VoipCallEventListener");
 
 			@Override
 			public void onRinging(String peerIdentity) {

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

@@ -227,7 +227,11 @@ public class AddContactActivity extends ThreemaActivity implements GenericAlertD
 					showContactDetail(identity);
 					finish();
 				} else if (exception instanceof InvalidEntryException){
-					GenericAlertDialog.newInstance(R.string.title_adduser, ((InvalidEntryException) exception).getTextId(), R.string.close, 0).show(getSupportFragmentManager(), DIALOG_TAG_ADD_ERROR);
+					GenericAlertDialog.newInstance(
+						ConfigUtils.isOnPremBuild() ?
+						R.string.invalid_onprem_id_title :
+						R.string.title_adduser,
+						((InvalidEntryException) exception).getTextId(), R.string.close, 0).show(getSupportFragmentManager(), DIALOG_TAG_ADD_ERROR);
 				} else if (exception instanceof PolicyViolationException) {
 					Toast.makeText(AddContactActivity.this, R.string.disabled_by_policy_short, Toast.LENGTH_SHORT).show();
 					finish();

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

@@ -28,7 +28,6 @@ import android.view.MenuItem;
 import com.google.android.material.tabs.TabLayout;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import androidx.appcompat.app.ActionBar;
 import androidx.fragment.app.Fragment;
@@ -44,11 +43,12 @@ import ch.threema.app.utils.AppRestrictionUtil;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.HiddenChatUtil;
 import ch.threema.app.utils.TestUtil;
+import ch.threema.base.utils.LoggingUtil;
 
 import static ch.threema.app.services.PreferenceService.LockingMech_NONE;
 
 public class BackupAdminActivity extends ThreemaToolbarActivity {
-	private static final Logger logger = LoggerFactory.getLogger(BackupAdminActivity.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("BackupAdminActivity");
 
 	private static final String BUNDLE_IS_UNLOCKED = "biu";
 

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

@@ -44,7 +44,6 @@ import com.google.android.material.appbar.CollapsingToolbarLayout;
 import com.google.android.material.floatingactionbutton.FloatingActionButton;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.util.Date;
@@ -98,6 +97,7 @@ import ch.threema.app.utils.ViewUtil;
 import ch.threema.app.voip.services.VoipStateService;
 import ch.threema.app.voip.util.VoipUtil;
 import ch.threema.base.ThreemaException;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.domain.models.VerificationLevel;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.GroupModel;
@@ -108,7 +108,7 @@ public class ContactDetailActivity extends ThreemaToolbarActivity
 		implements LifecycleOwner,
 					GenericAlertDialog.DialogClickListener,
 		            ContactEditDialog.ContactEditDialogClickListener {
-	private static final Logger logger = LoggerFactory.getLogger(ContactDetailActivity.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("ContactDetailActivity");
 
 	private static final String DIALOG_TAG_EDIT = "cedit";
 	private static final String DIALOG_TAG_DELETE_CONTACT = "deleteContact";
@@ -884,11 +884,6 @@ public class ContactDetailActivity extends ThreemaToolbarActivity
 			case DIALOG_TAG_DELETE_CONTACT:
 				ContactModel contactModel = (ContactModel)data;
 				deleteContact(contactModel);
-				try {
-					serviceManager.getShortcutService().deletePinnedShortcut(contactService.createReceiver(contactModel));
-				} catch (ThreemaException e) {
-					logger.error("Exception, failed to delete direct share shortcut", e);
-				}
 				break;
 			case DIALOG_TAG_EXCLUDE_CONTACT:
 				removeContactConfirmed(true, (ContactModel) data);

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

@@ -40,7 +40,6 @@ import com.google.android.material.chip.Chip;
 import com.google.android.material.chip.ChipGroup;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -69,12 +68,13 @@ 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.base.utils.LoggingUtil;
 import ch.threema.domain.protocol.api.work.WorkDirectoryCategory;
 import ch.threema.domain.protocol.api.work.WorkDirectoryContact;
 import ch.threema.domain.protocol.api.work.WorkOrganization;
 
 public class DirectoryActivity extends ThreemaToolbarActivity implements ThreemaSearchView.OnQueryTextListener, MultiChoiceSelectorDialog.SelectorDialogClickListener {
-	private static final Logger logger = LoggerFactory.getLogger(DirectoryActivity.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("DirectoryActivity");
 
 	private static final int API_DIRECTORY_PAGE_SIZE = 3;
 	private static final long QUERY_TIMEOUT = 1000; // ms

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

@@ -37,7 +37,6 @@ import android.view.Gravity;
 import android.widget.Toast;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
@@ -47,15 +46,15 @@ import ch.threema.app.R;
 import ch.threema.app.dialogs.GenericAlertDialog;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.RuntimeUtil;
+import ch.threema.base.utils.LoggingUtil;
 
 import static ch.threema.app.fragments.BackupDataFragment.REQUEST_ID_DISABLE_BATTERY_OPTIMIZATIONS;
 
 /**
  * Guides user through the process of disabling battery optimization energy saving option.
  */
-
 public class DisableBatteryOptimizationsActivity extends AppCompatActivity implements GenericAlertDialog.DialogClickListener {
-	private static final Logger logger = LoggerFactory.getLogger(DisableBatteryOptimizationsActivity.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("DisableBatteryOptimizationsActivity");
 
 	private static final int REQUEST_CODE_IGNORE_BATTERY_OPTIMIZATIONS = 778;
 	private static final String DIALOG_TAG_DISABLE_BATTERY_OPTIMIZATIONS = "des";

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

@@ -43,7 +43,6 @@ import com.google.android.material.appbar.CollapsingToolbarLayout;
 import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -101,7 +100,7 @@ import ch.threema.app.utils.NameUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.voip.util.VoipUtil;
-import ch.threema.base.ThreemaException;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.localcrypto.MasterKeyLockedException;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.GroupModel;
@@ -113,7 +112,7 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 	TextEntryDialog.TextEntryDialogClickListener,
 	GroupDetailAdapter.OnGroupDetailsClickListener
 	{
-	private static final Logger logger = LoggerFactory.getLogger(GroupDetailActivity.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("GroupDetailActivity");
 	// static values
 	private final int MODE_EDIT = 1;
 	private final int MODE_READONLY = 2;
@@ -865,11 +864,6 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 				break;
 			case DIALOG_TAG_DELETE_GROUP:
 				deleteGroupAndQuit();
-				try {
-					serviceManager.getShortcutService().deletePinnedShortcut(groupService.createReceiver(groupModel));
-				} catch (ThreemaException e) {
-					logger.debug("Exception, failed to delete direct group shortcut", e);
-				}
 				break;
 			case DIALOG_TAG_QUIT:
 				saveGroupSettings();

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

@@ -56,7 +56,6 @@ import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
 import com.mapbox.mapboxsdk.maps.Style;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -81,6 +80,7 @@ import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.GeoLocationUtil;
 import ch.threema.app.utils.LocationUtil;
 import ch.threema.app.utils.RuntimeUtil;
+import ch.threema.base.utils.LoggingUtil;
 
 import static ch.threema.app.utils.IntentDataUtil.INTENT_DATA_LOCATION_LAT;
 import static ch.threema.app.utils.IntentDataUtil.INTENT_DATA_LOCATION_LNG;
@@ -88,8 +88,7 @@ import static ch.threema.app.utils.IntentDataUtil.INTENT_DATA_LOCATION_NAME;
 import static ch.threema.app.utils.IntentDataUtil.INTENT_DATA_LOCATION_PROVIDER;
 
 public class MapActivity extends ThreemaActivity implements GenericAlertDialog.DialogClickListener {
-
-	private static final Logger logger = LoggerFactory.getLogger(MapActivity.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("MapActivity");
 
 	private static final String DIALOG_TAG_ENABLE_LOCATION_SERVICES = "lss";
 	private static final String DIALOG_TAG_PRIVACY_POLICY_40_ACCEPT = "40acc";

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

@@ -33,10 +33,12 @@ import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.BadParcelableException;
 import android.os.BaseBundle;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Parcelable;
 import android.provider.DocumentsContract;
 import android.text.TextUtils;
+import android.text.format.DateUtils;
 import android.util.SparseArray;
 import android.view.Menu;
 import android.view.MenuItem;
@@ -101,7 +103,6 @@ import ch.threema.app.services.FileService;
 import ch.threema.app.services.GroupService;
 import ch.threema.app.services.MessageService;
 import ch.threema.app.services.PreferenceService;
-import ch.threema.app.services.ShortcutService;
 import ch.threema.app.services.UserService;
 import ch.threema.app.ui.MediaItem;
 import ch.threema.app.ui.SingleToast;
@@ -115,6 +116,7 @@ import ch.threema.app.utils.MimeUtil;
 import ch.threema.app.utils.NameUtil;
 import ch.threema.app.utils.NavigationUtil;
 import ch.threema.app.utils.RuntimeUtil;
+import ch.threema.app.utils.ShortcutUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.domain.protocol.csp.messages.file.FileData;
 import ch.threema.storage.models.AbstractMessageModel;
@@ -168,7 +170,6 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 	private DistributionListService distributionListService;
 	private MessageService messageService;
 	private FileService fileService;
-	private ShortcutService shortcutService;
 
 	private final Runnable copyFilesRunnable = new Runnable() {
 		@Override
@@ -246,7 +247,6 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 			this.distributionListService = serviceManager.getDistributionListService();
 			this.messageService = serviceManager.getMessageService();
 			this.fileService = serviceManager.getFileService();
-			this.shortcutService = serviceManager.getShortcutService();
 			userService = serviceManager.getUserService();
 		} catch (Exception e) {
 			logger.error("Exception", e);
@@ -412,7 +412,7 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 				// maybe a shortcut?
 				String id = intent.getStringExtra(ShortcutManagerCompat.EXTRA_SHORTCUT_ID);
 				if (!TestUtil.empty(id)) {
-					BaseBundle bundle = shortcutService.getShareTargetExtrasFromShortcutId(id);
+					BaseBundle bundle = ShortcutUtil.getShareTargetExtrasFromShortcutId(id);
 					if (bundle != null) {
 						if (bundle.containsKey(ThreemaApplication.INTENT_DATA_CONTACT)) {
 							identity = bundle.getString(ThreemaApplication.INTENT_DATA_CONTACT);
@@ -721,7 +721,9 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 				getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
 			} catch (Exception e) {
 				logger.info("Unable to take persistable uri permission");
-				uri = FileUtil.getFileUri(uri);
+				if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+					uri = FileUtil.getFileUri(uri);
+				}
 			}
 		}
 		mediaItems.add(new MediaItem(uri, mimeType, caption));
@@ -850,13 +852,13 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 
 					switch (messageModel.getType()) {
 						case IMAGE:
-							sendForwardedMedia(messageReceivers, uri, caption, MediaItem.TYPE_IMAGE, null, FileData.RENDERING_MEDIA, null);
+							sendForwardedMedia(messageReceivers, uri, caption, MediaItem.TYPE_IMAGE, null, FileData.RENDERING_MEDIA, null, 0L);
 							break;
 						case VIDEO:
-							sendForwardedMedia(messageReceivers, uri, caption, MediaItem.TYPE_VIDEO, null, FileData.RENDERING_MEDIA, null);
+							sendForwardedMedia(messageReceivers, uri, caption, MediaItem.TYPE_VIDEO, null, FileData.RENDERING_MEDIA, null, messageModel.getVideoData().getDuration() * DateUtils.SECOND_IN_MILLIS);
 							break;
 						case VOICEMESSAGE:
-							sendForwardedMedia(messageReceivers, uri, caption, MediaItem.TYPE_VOICEMESSAGE, MimeUtil.MIME_TYPE_AUDIO_AAC, FileData.RENDERING_MEDIA, null);
+							sendForwardedMedia(messageReceivers, uri, caption, MediaItem.TYPE_VOICEMESSAGE, MimeUtil.MIME_TYPE_AUDIO_AAC, FileData.RENDERING_MEDIA, null, messageModel.getAudioData().getDuration() * DateUtils.SECOND_IN_MILLIS);
 							break;
 						case FILE:
 							int mediaType = MediaItem.TYPE_FILE;
@@ -866,7 +868,7 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 							if (messageModel.getFileData().getRenderingType() != FileData.RENDERING_DEFAULT) {
 								mediaType = MimeUtil.getMediaTypeFromMimeType(mimeType);
 							}
-							sendForwardedMedia(messageReceivers, uri, caption, mediaType, mimeType, renderingType, messageModel.getFileData().getFileName());
+							sendForwardedMedia(messageReceivers, uri, caption, mediaType, mimeType, renderingType, messageModel.getFileData().getFileName(), messageModel.getFileData().getDurationMs());
 							break;
 						case LOCATION:
 							sendLocationMessage(messageReceivers, messageModel.getLocationData());
@@ -1172,7 +1174,7 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 	 */
 
 	@AnyThread
-	private void sendForwardedMedia(final MessageReceiver[] messageReceivers, final Uri uri, final String caption, final int type, @Nullable final String mimeType, @FileData.RenderingType final int renderingType, final String filename) {
+	private void sendForwardedMedia(final MessageReceiver[] messageReceivers, final Uri uri, final String caption, final int type, @Nullable final String mimeType, @FileData.RenderingType final int renderingType, final String filename, long durationMs) {
 		final MediaItem mediaItem = new MediaItem(uri, type);
 		if (mimeType != null) {
 			mediaItem.setMimeType(mimeType);
@@ -1193,6 +1195,9 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 				// do not scale forwarded images
 				mediaItem.setImageScale(PreferenceService.ImageScale_ORIGINAL);
 			}
+			else if (type == MediaItem.TYPE_VOICEMESSAGE) {
+				mediaItem.setDurationMs(durationMs);
+			}
 		}
 		messageService.sendMediaSingleThread(Collections.singletonList(mediaItem), Arrays.asList(messageReceivers));
 	}

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

@@ -63,7 +63,6 @@ import android.widget.Toast;
 import com.google.android.material.snackbar.Snackbar;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.io.IOException;
@@ -115,6 +114,7 @@ import ch.threema.app.utils.MimeUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.video.VideoTimelineCache;
 import ch.threema.base.ThreemaException;
+import ch.threema.base.utils.LoggingUtil;
 import pl.droidsonroids.gif.GifImageView;
 
 import static ch.threema.app.adapters.SendMediaGridAdapter.VIEW_TYPE_ADD;
@@ -130,7 +130,7 @@ public class SendMediaActivity extends ThreemaToolbarActivity implements
 	GenericAlertDialog.DialogClickListener,
 	ThreemaToolbarActivity.OnSoftKeyboardChangedListener {
 
-	private static final Logger logger = LoggerFactory.getLogger(SendMediaActivity.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("SendMediaActivity");
 
 	private static final String STATE_BIGIMAGE_POS = "bigimage_pos";
 	private static final String STATE_ITEMS = "items";

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

@@ -33,6 +33,7 @@ import android.widget.Button;
 import android.widget.FrameLayout;
 import android.widget.ProgressBar;
 import android.widget.TextView;
+import android.widget.Toast;
 
 import com.google.android.material.snackbar.Snackbar;
 import com.google.android.material.textfield.MaterialAutoCompleteTextView;
@@ -58,6 +59,7 @@ import ch.threema.app.managers.ListenerManager;
 import ch.threema.app.services.ConversationService;
 import ch.threema.app.services.FileService;
 import ch.threema.app.services.MessageService;
+import ch.threema.app.services.UserService;
 import ch.threema.app.utils.DialogUtil;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.ConversationModel;
@@ -76,6 +78,7 @@ public class StorageManagementActivity extends ThreemaToolbarActivity implements
 	private FileService fileService;
 	private MessageService messageService;
 	private ConversationService conversationService;
+	private UserService userService;
 	private TextView totalView, usageView, freeView, messageView, inuseView;
 	private MaterialAutoCompleteTextView timeSpinner, messageTimeSpinner;
 	private Button deleteButton, messageDeleteButton;
@@ -100,12 +103,19 @@ public class StorageManagementActivity extends ThreemaToolbarActivity implements
 			this.fileService = serviceManager.getFileService();
 			this.messageService = serviceManager.getMessageService();
 			this.conversationService = serviceManager.getConversationService();
+			this.userService = serviceManager.getUserService();
 		} catch (Exception e) {
 			logger.error("Exception", e);
 			finish();
 			return;
 		}
 
+		if (!this.userService.hasIdentity()) {
+			Toast.makeText(this, "Nothing to delete!", Toast.LENGTH_SHORT).show();
+			finish();
+			return;
+		}
+
 		coordinatorLayout = findViewById(R.id.content);
 		totalView = findViewById(R.id.total_view);
 		usageView = findViewById(R.id.usage_view);

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

@@ -37,7 +37,6 @@ import android.widget.TextView;
 import com.google.android.material.card.MaterialCardView;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import androidx.annotation.ColorInt;
 import androidx.annotation.LayoutRes;
@@ -58,10 +57,11 @@ import ch.threema.app.utils.MessageUtil;
 import ch.threema.app.utils.QuoteUtil;
 import ch.threema.app.utils.StateBitmapUtil;
 import ch.threema.base.ThreemaException;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.storage.models.AbstractMessageModel;
 
 public class TextChatBubbleActivity extends ThreemaActivity implements GenericAlertDialog.DialogClickListener {
-	private static final Logger logger = LoggerFactory.getLogger(TextChatBubbleActivity.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("TextChatBubbleActivity");
 
 	private static final int CONTEXT_MENU_FORWARD = 600;
 	private static final int CONTEXT_MENU_GROUP = 22200;
@@ -229,7 +229,7 @@ public class TextChatBubbleActivity extends ThreemaActivity implements GenericAl
 
 	private void setText(AbstractMessageModel messageModel) {
 		textView.setText(QuoteUtil.getMessageBody(messageModel, false));
-		LinkifyUtil.getInstance().linkify(null, this, textView, messageModel, messageModel.getBody().length() < 80, false, null);
+		LinkifyUtil.getInstance().linkify(null, this, textView, messageModel, true, false, null);
 	}
 
 	@Override

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

@@ -26,7 +26,6 @@ import android.os.Bundle;
 import android.widget.Toast;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
@@ -34,9 +33,10 @@ import ch.threema.app.backuprestore.csv.BackupService;
 import ch.threema.app.backuprestore.csv.RestoreService;
 import ch.threema.app.services.UserService;
 import ch.threema.app.utils.TestUtil;
+import ch.threema.base.utils.LoggingUtil;
 
 public abstract class ThreemaActivity extends ThreemaAppCompatActivity {
-	private static final Logger logger = LoggerFactory.getLogger(ThreemaActivity.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("ThreemaActivity");
 
 	final static public int ACTIVITY_ID_WIZARDFIRST = 20001;
 	final static public int ACTIVITY_ID_SETTINGS = 20002;

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

@@ -33,7 +33,6 @@ import android.widget.Toast;
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.net.InetSocketAddress;
 import java.util.HashSet;
@@ -54,6 +53,7 @@ import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.ConnectionIndicatorUtil;
 import ch.threema.app.utils.EditTextUtil;
 import ch.threema.app.utils.RuntimeUtil;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.domain.protocol.csp.connection.ConnectionState;
 import ch.threema.domain.protocol.csp.connection.ConnectionStateListener;
 import ch.threema.domain.protocol.csp.connection.ThreemaConnection;
@@ -63,7 +63,7 @@ import ch.threema.localcrypto.MasterKey;
  * Helper class for activities that use the new toolbar
  */
 public abstract class ThreemaToolbarActivity extends ThreemaActivity implements ConnectionStateListener {
-	private static final Logger logger = LoggerFactory.getLogger(ThreemaToolbarActivity.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("ThreemaToolbarActivity");
 
 	private Toolbar toolbar;
 	private View connectionIndicator;

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

@@ -87,7 +87,8 @@ public class WizardIntroActivity extends WizardBackgroundActivity {
 		frameAnimation.start();
 
 		TextView privacyPolicyExplainText = findViewById(R.id.wizard_privacy_policy_explain);
-		if (TestUtil.empty(ThreemaApplication.getAppContext().getString(R.string.privacy_policy_url))) {
+		if (TestUtil.empty(ThreemaApplication.getAppContext().getString(R.string.privacy_policy_url)) ||
+			(ConfigUtils.isOnPremBuild() && !ConfigUtils.isDemoOPServer(preferenceService))) {
 			privacyPolicyExplainText.setVisibility(View.GONE);
 		} else {
 			String privacyPolicy = getString(R.string.privacy_policy);

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

@@ -192,7 +192,7 @@ public class MediaGalleryAdapter extends ArrayAdapter<AbstractMessageModel> {
 									holder.imageView.setColorFilter(foregroundColor, PorterDuff.Mode.SRC_IN);
 									holder.topTextView.setText(StringConversionUtil.secondsToString(
 										messageModel.getType() == MessageType.FILE ?
-											messageModel.getFileData().getDuration():
+											messageModel.getFileData().getDurationSeconds():
 											messageModel.getAudioData().getDuration(), false));
 									holder.textContainerView.setVisibility(View.VISIBLE);
 								} else if (messageModel.getType() == MessageType.FILE) {

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

@@ -121,7 +121,7 @@ public class AudioChatAdapterDecorator extends ChatAdapterDecorator {
 			isDownloaded = audioDataModel.isDownloaded();
 		} else {
 			fileDataModel = this.getMessageModel().getFileData();
-			duration = fileDataModel.getDuration();
+			duration = fileDataModel.getDurationSeconds();
 			isDownloaded = fileDataModel.isDownloaded();
 			caption = fileDataModel.getCaption();
 		}
@@ -393,7 +393,7 @@ public class AudioChatAdapterDecorator extends ChatAdapterDecorator {
 				(ComposeMessageFragment) helper.getFragment(),
 				holder.bodyTextView,
 				this.getMessageModel(),
-				caption.length() < 80,
+				true,
 				actionModeStatus.getActionModeEnabled(),
 				onClickElement);
 

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

@@ -192,7 +192,7 @@ public class FileChatAdapterDecorator extends ChatAdapterDecorator {
 				(ComposeMessageFragment) helper.getFragment(),
 				holder.bodyTextView,
 				this.getMessageModel(),
-				fileData.getCaption().length() < 80,
+				true,
 				actionModeStatus.getActionModeEnabled(),
 				onClickElement);
 

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

@@ -141,7 +141,7 @@ public class ImageChatAdapterDecorator extends ChatAdapterDecorator {
 				(ComposeMessageFragment) helper.getFragment(),
 				holder.bodyTextView,
 				this.getMessageModel(),
-				getMessageModel().getCaption().length() < 80,
+				true,
 				actionModeStatus.getActionModeEnabled(),
 				onClickElement);
 

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

@@ -100,7 +100,7 @@ public class TextChatAdapterDecorator extends ChatAdapterDecorator {
 				(ComposeMessageFragment) helper.getFragment(),
 				holder.bodyTextView,
 				this.getMessageModel(),
-				messageText.length() < 80,
+				true,
 				actionModeStatus.getActionModeEnabled(),
 				onClickElement);
 		}

+ 8 - 17
app/src/main/java/ch/threema/app/adapters/decorators/VideoChatAdapterDecorator.java

@@ -50,8 +50,6 @@ import ch.threema.storage.models.DistributionListMessageModel;
 import ch.threema.storage.models.MessageState;
 import ch.threema.storage.models.MessageType;
 
-import static ch.threema.storage.models.data.media.FileDataModel.METADATA_KEY_DURATION;
-
 public class VideoChatAdapterDecorator extends ChatAdapterDecorator {
 	private static final Logger logger = LoggerFactory.getLogger(VideoChatAdapterDecorator.class);
 
@@ -158,27 +156,17 @@ public class VideoChatAdapterDecorator extends ChatAdapterDecorator {
 
 			if (size > 0) {
 				datePrefixString += " (" + Formatter.formatShortFileSize(getContext(), size) + ")";
+				this.dateContentDescriptionPreifx = getContext().getString(R.string.file_size) + ": " + Formatter.formatShortFileSize(getContext(), size);
 			}
 
 			this.setDatePrefix(datePrefixString, holder.dateView.getTextSize());
 
 			setDefaultBackground(holder);
 		} else if (this.getMessageModel().getType() == MessageType.FILE && this.getMessageModel().getFileData() != null) {
-			String datePrefixString = "";
-			long duration = 0;
-
-			Float durationF = this.getMessageModel().getFileData().getMetaDataFloat(METADATA_KEY_DURATION);
-			if (durationF != null) {
-				duration = durationF.longValue();
-				if (duration > 0) {
-					datePrefixString = StringConversionUtil.secondsToString(duration, false);
-					this.dateContentDescriptionPreifx = getContext().getString(R.string.duration) + ": " + StringConversionUtil.getDurationStringHuman(getContext(), duration);
-				}
-			}
+			String datePrefixString = this.getMessageModel().getFileData().getDurationString();
+			long duration = this.getMessageModel().getFileData().getDurationSeconds();
 
-			if (this.getMessageModel().getFileData().isDownloaded()) {
-				datePrefixString = "";
-			} else {
+			if (!this.getMessageModel().getFileData().isDownloaded()) {
 				long size = this.getMessageModel().getFileData().getFileSize();
 				if (size > 0) {
 					if (duration > 0) {
@@ -186,7 +174,10 @@ public class VideoChatAdapterDecorator extends ChatAdapterDecorator {
 					} else {
 						datePrefixString = Formatter.formatShortFileSize(getContext(), size);
 					}
+					this.dateContentDescriptionPreifx = getContext().getString(R.string.file_size) + ": " + Formatter.formatShortFileSize(getContext(), size);
 				}
+			} else {
+				this.dateContentDescriptionPreifx = getContext().getString(R.string.duration) + ": " + StringConversionUtil.getDurationStringHuman(getContext(), duration);
 			}
 
 			if (holder.dateView != null) {
@@ -202,7 +193,7 @@ public class VideoChatAdapterDecorator extends ChatAdapterDecorator {
 					(ComposeMessageFragment) helper.getFragment(),
 					holder.bodyTextView,
 					this.getMessageModel(),
-					this.getMessageModel().getFileData().getCaption().length() < 80,
+					true,
 					actionModeStatus.getActionModeEnabled(),
 					onClickElement);
 

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

@@ -31,17 +31,17 @@ 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;
 import ch.threema.app.managers.ServiceManager;
+import ch.threema.app.push.PushService;
 import ch.threema.app.services.PassphraseService;
 import ch.threema.app.utils.DialogUtil;
 import ch.threema.app.utils.SecureDeleteUtil;
+import ch.threema.app.utils.ShortcutUtil;
 import ch.threema.app.webclient.services.SessionWakeUpServiceImpl;
 import ch.threema.app.webclient.services.instance.DisconnectContext;
-import ch.threema.domain.protocol.csp.connection.ThreemaConnection;
 import ch.threema.storage.DatabaseServiceNew;
 import ch.threema.storage.NonceDatabaseBlobService;
 
@@ -69,8 +69,6 @@ public class DeleteIdentityAsyncTask extends AsyncTask<Void, Void, Exception> {
 
 	@Override
 	protected Exception doInBackground(Void... params) {
-		ThreemaConnection connection = serviceManager.getConnection();
-
 		try {
 			// clear push token
 			PushService.deleteToken(ThreemaApplication.getAppContext());
@@ -86,6 +84,8 @@ public class DeleteIdentityAsyncTask extends AsyncTask<Void, Void, Exception> {
 			serviceManager.getPreferenceService().clear();
 			serviceManager.getFileService().removeAllAvatars();
 			serviceManager.getWallpaperService().removeAll(ThreemaApplication.getAppContext(), true);
+			ShortcutUtil.deleteAllShareTargetShortcuts();
+			ShortcutUtil.deleteAllPinnedShortcuts();
 
 			boolean interrupted = false;
 

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

@@ -24,7 +24,6 @@ package ch.threema.app.backuprestore.csv;
 import android.content.Context;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.io.FilenameFilter;
@@ -38,9 +37,10 @@ import java.util.List;
 import ch.threema.app.backuprestore.BackupRestoreDataService;
 import ch.threema.app.services.FileService;
 import ch.threema.base.ThreemaException;
+import ch.threema.base.utils.LoggingUtil;
 
 public class BackupRestoreDataServiceImpl implements BackupRestoreDataService {
-	private static final Logger logger = LoggerFactory.getLogger("BackupRestoreDataServiceImpl");
+	private static final Logger logger = LoggingUtil.getThreemaLogger("BackupRestoreDataServiceImpl");
 
 	private final Context context;
 	private final FileService fileService;

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

@@ -43,7 +43,6 @@ import net.lingala.zip4j.model.FileHeader;
 
 import org.apache.commons.io.IOUtils;
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.io.IOException;
@@ -85,6 +84,7 @@ import ch.threema.app.utils.MimeUtil;
 import ch.threema.app.utils.StringConversionUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.base.ThreemaException;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.base.utils.Utils;
 import ch.threema.domain.models.GroupId;
 import ch.threema.domain.models.VerificationLevel;
@@ -117,7 +117,7 @@ import static ch.threema.app.services.NotificationService.NOTIFICATION_CHANNEL_A
 import static ch.threema.app.services.NotificationService.NOTIFICATION_CHANNEL_BACKUP_RESTORE_IN_PROGRESS;
 
 public class RestoreService extends Service {
-	private static final Logger logger = LoggerFactory.getLogger("RestoreService");
+	private static final Logger logger = LoggingUtil.getThreemaLogger("RestoreService");
 
 	public static final String EXTRA_RESTORE_BACKUP_FILE = "file";
 	public static final String EXTRA_RESTORE_BACKUP_PASSWORD = "pwd";

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

@@ -33,7 +33,6 @@ import android.content.res.AssetFileDescriptor;
 import android.content.res.Configuration;
 import android.graphics.Bitmap;
 import android.graphics.Paint;
-import android.graphics.Typeface;
 import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.media.MediaPlayer;
@@ -75,8 +74,6 @@ import android.widget.ScrollView;
 import android.widget.TextView;
 import android.widget.Toast;
 
-import com.getkeepsafe.taptargetview.TapTarget;
-import com.getkeepsafe.taptargetview.TapTargetView;
 import com.google.android.material.snackbar.Snackbar;
 
 import org.slf4j.Logger;
@@ -183,7 +180,6 @@ import ch.threema.app.services.MessageService;
 import ch.threema.app.services.NotificationService;
 import ch.threema.app.services.PreferenceService;
 import ch.threema.app.services.RingtoneService;
-import ch.threema.app.services.ShortcutService;
 import ch.threema.app.services.UserService;
 import ch.threema.app.services.WallpaperService;
 import ch.threema.app.services.ballot.BallotService;
@@ -223,6 +219,7 @@ import ch.threema.app.utils.NameUtil;
 import ch.threema.app.utils.NavigationUtil;
 import ch.threema.app.utils.QuoteUtil;
 import ch.threema.app.utils.RuntimeUtil;
+import ch.threema.app.utils.ShortcutUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.ToolbarUtil;
 import ch.threema.app.voicemessage.VoiceRecorderActivity;
@@ -252,6 +249,7 @@ import static ch.threema.app.services.messageplayer.MessagePlayer.SOURCE_AUDIORE
 import static ch.threema.app.services.messageplayer.MessagePlayer.SOURCE_LIFECYCLE;
 import static ch.threema.app.services.messageplayer.MessagePlayer.SOURCE_VOIP;
 import static ch.threema.app.utils.LinkifyUtil.DIALOG_TAG_CONFIRM_LINK;
+import static ch.threema.app.utils.ShortcutUtil.TYPE_CHAT;
 
 public class ComposeMessageFragment extends Fragment implements
 	LifecycleOwner,
@@ -348,7 +346,6 @@ public class ComposeMessageFragment extends Fragment implements
 	private UserService userService;
 	private FileService fileService;
 	private VoipStateService voipStateService;
-	private ShortcutService shortcutService;
 	private DownloadService downloadService;
 	private LicenseService licenseService;
 
@@ -572,14 +569,16 @@ public class ComposeMessageFragment extends Fragment implements
 
 		@Override
 		public void onRemoved(List<AbstractMessageModel> removedMessageModels) {
-			if (TestUtil.required(composeMessageAdapter, removedMessageModels)) {
-				for (AbstractMessageModel removedMessageModel: removedMessageModels) {
-					composeMessageAdapter.remove(removedMessageModel);
+			RuntimeUtil.runOnUiThread(() -> {
+				if (TestUtil.required(composeMessageAdapter, removedMessageModels)) {
+					for (AbstractMessageModel removedMessageModel : removedMessageModels) {
+						composeMessageAdapter.remove(removedMessageModel);
+					}
+					RuntimeUtil.runOnUiThread(() -> {
+						composeMessageAdapter.notifyDataSetChanged();
+					});
 				}
-				RuntimeUtil.runOnUiThread(() -> {
-					composeMessageAdapter.notifyDataSetChanged();
-				});
-			}
+			});
 		}
 
 		@Override
@@ -1834,58 +1833,6 @@ public class ComposeMessageFragment extends Fragment implements
 							}, 1000);
 						}
 					}
-				} else {
-					if (!preferenceService.getIsVideoCallTooltipShown()) {
-						if (ContactUtil.canReceiveVoipMessages(contactModel, blackListIdentityService)
-							&& ConfigUtils.isCallsEnabled(getContext(), preferenceService, licenseService)) {
-							View toolbar = ((ThreemaToolbarActivity) getActivity()).getToolbar();
-
-							toolbar.postDelayed(() -> {
-								if (getActivity() != null && isAdded()) {
-									int[] location = new int[2];
-									View itemView = toolbar.findViewById(R.id.menu_threema_call);
-									if (itemView != null) {
-										itemView.getLocationInWindow(location);
-										if (ConfigUtils.isVideoCallsEnabled()) {
-											try {
-												TapTargetView.showFor(getActivity(),
-													TapTarget.forView(itemView, getString(R.string.video_calls_new), getString(R.string.tooltip_video_call))
-														.outerCircleColor(ConfigUtils.getAppTheme(getActivity()) == ConfigUtils.THEME_DARK ? R.color.dark_accent : R.color.accent_light)      // Specify a color for the outer circle
-														.outerCircleAlpha(0.96f)            // Specify the alpha amount for the outer circle
-														.targetCircleColor(android.R.color.white)   // Specify a color for the target circle
-														.titleTextSize(24)                  // Specify the size (in sp) of the title text
-														.titleTextColor(android.R.color.white)      // Specify the color of the title text
-														.descriptionTextSize(18)            // Specify the size (in sp) of the description text
-														.descriptionTextColor(android.R.color.white)  // Specify the color of the description text
-														.textColor(android.R.color.white)            // Specify a color for both the title and description text
-														.textTypeface(Typeface.SANS_SERIF)  // Specify a typeface for the text
-														.dimColor(android.R.color.black)            // If set, will dim behind the view with 30% opacity of the given color
-														.drawShadow(true)                   // Whether to draw a drop shadow or not
-														.cancelable(true)                  // Whether tapping outside the outer circle dismisses the view
-														.tintTarget(true)                   // Whether to tint the target view's color
-														.transparentTarget(false)           // Specify whether the target is transparent (displays the content underneath)
-														.targetRadius(50),                  // Specify the target radius (in dp)
-													new TapTargetView.Listener() {          // The listener can listen for regular clicks, long clicks or cancels
-														@Override
-														public void onTargetClick(TapTargetView view) {
-															super.onTargetClick(view);
-															String name = NameUtil.getDisplayNameOrNickname(contactModel, false);
-
-															GenericAlertDialog dialog = GenericAlertDialog.newInstance(R.string.threema_call, String.format(getContext().getString(R.string.voip_call_confirm), name), R.string.ok, R.string.cancel);
-															dialog.setTargetFragment(ComposeMessageFragment.this, 0);
-															dialog.show(getFragmentManager(), ComposeMessageFragment.DIALOG_TAG_CONFIRM_CALL);
-														}
-													});
-												preferenceService.setVideoCallTooltipShown(true);
-											} catch (Exception ignore) {
-												// catch null typeface exception on CROSSCALL Action-X3
-											}
-										}
-									}
-								}
-							}, 1000);
-						}
-					}
 				}
 			}
 		}
@@ -3800,7 +3747,7 @@ public class ComposeMessageFragment extends Fragment implements
 				selectorDialog.setTargetFragment(this, 0);
 				selectorDialog.show(getFragmentManager(), DIALOG_TAG_CHOOSE_SHORTCUT_TYPE);
 		} else {
-			this.shortcutService.createPinnedShortcut(messageReceiver, ShortcutService.TYPE_CHAT);
+			ShortcutUtil.createPinnedShortcut(messageReceiver, TYPE_CHAT);
 		}
 	}
 
@@ -3849,7 +3796,7 @@ public class ComposeMessageFragment extends Fragment implements
 	public void onClick(String tag, int which, Object data) {
 		if (DIALOG_TAG_CHOOSE_SHORTCUT_TYPE.equals(tag)) {
 			int shortcutType = which + 1;
-			this.shortcutService.createPinnedShortcut(messageReceiver, shortcutType);
+			ShortcutUtil.createPinnedShortcut(messageReceiver, shortcutType);
 		}
 	}
 
@@ -4393,8 +4340,7 @@ public class ComposeMessageFragment extends Fragment implements
 				this.wallpaperService,
 				this.mutedChatsListService,
 				this.ringtoneService,
-				this.voipStateService,
-				this.shortcutService
+				this.voipStateService
 		);
 	}
 
@@ -4422,7 +4368,6 @@ public class ComposeMessageFragment extends Fragment implements
 				this.hiddenChatsListService = serviceManager.getHiddenChatsListService();
 				this.ringtoneService = serviceManager.getRingtoneService();
 				this.voipStateService = serviceManager.getVoipStateService();
-				this.shortcutService = serviceManager.getShortcutService();
 				this.downloadService = serviceManager.getDownloadService();
 				this.licenseService = serviceManager.getLicenseService();
 			} catch (Exception e) {
@@ -4442,7 +4387,6 @@ public class ComposeMessageFragment extends Fragment implements
 						@Override
 						public void run() {
 							distributionListService.remove(dmodel);
-							shortcutService.deletePinnedShortcut(distributionListService.createReceiver(dmodel));
 							RuntimeUtil.runOnUiThread(() -> activity.finish());
 						}
 					}).start();

+ 52 - 21
app/src/main/java/ch/threema/app/fragments/MessageSectionFragment.java

@@ -218,8 +218,7 @@ public class MessageSectionFragment extends MainFragment
 	private int cornerRadius;
 	private TagModel unreadTagModel;
 
-	private int archiveCount = 0;
-	private Snackbar archiveSnackbar;
+	private ArchiveSnackbar archiveSnackbar;
 
 	private ConversationModel selectedConversation;
 	private ExtendedFloatingActionButton floatingButtonView;
@@ -875,28 +874,10 @@ public class MessageSectionFragment extends MainFragment
 							}
 						});
 					} else if (direction == ItemTouchHelper.LEFT) {
-						archiveCount++;
 						ConversationModel archiveableConversation = holder.getConversationModel();
 						conversationService.archive(archiveableConversation);
 
-						String snackText = String.format(getString(R.string.message_archived), archiveCount);
-
-						if (archiveSnackbar != null && archiveSnackbar.isShown()) {
-							archiveSnackbar.dismiss();
-						}
-
-						if (getView() != null) {
-							archiveSnackbar = Snackbar.make(getView(), snackText, 7 * (int) DateUtils.SECOND_IN_MILLIS);
-							archiveSnackbar.setAction(R.string.undo, v -> conversationService.unarchive(Collections.singletonList(archiveableConversation)));
-							archiveSnackbar.addCallback(new Snackbar.Callback() {
-								@Override
-								public void onDismissed(Snackbar snackbar, int event) {
-									super.onDismissed(snackbar, event);
-									archiveCount = 0;
-								}
-							});
-							archiveSnackbar.show();
-						}
+						archiveSnackbar = new ArchiveSnackbar(archiveSnackbar, archiveableConversation);
 					}
  				}
 
@@ -1654,4 +1635,54 @@ public class MessageSectionFragment extends MainFragment
 			this.recyclerView.scrollToPosition(0);
 		}
 	}
+
+	/**
+	 * Keeps track of the last archive chats. This class is used for the undo action.
+	 */
+	private class ArchiveSnackbar {
+		private final Snackbar snackbar;
+		private final List<ConversationModel> conversationModels;
+
+		/**
+		 * Creates an updated archive snackbar, dismisses the old snackbar (if available), and shows
+		 * the updated snackbar.
+		 * @param archiveSnackbar the currently shown archive snackbar (if available)
+		 * @param archivedConversation the conversation that just has been archived
+		 */
+		ArchiveSnackbar(@Nullable ArchiveSnackbar archiveSnackbar, ConversationModel archivedConversation) {
+			this.conversationModels = new ArrayList<>();
+			this.conversationModels.add(archivedConversation);
+
+			if (archiveSnackbar != null) {
+				this.conversationModels.addAll(archiveSnackbar.conversationModels);
+				archiveSnackbar.dismiss();
+			}
+
+			if (getView() != null) {
+				String snackText = String.format(getString(R.string.message_archived), this.conversationModels.size());
+				this.snackbar = Snackbar.make(getView(), snackText, 7 * (int) DateUtils.SECOND_IN_MILLIS);
+
+				this.snackbar.setAction(R.string.undo, v -> conversationService.unarchive(conversationModels));
+				this.snackbar.addCallback(new Snackbar.Callback() {
+					@Override
+					public void onDismissed(Snackbar snackbar, int event) {
+						super.onDismissed(snackbar, event);
+						if (MessageSectionFragment.this.archiveSnackbar == ArchiveSnackbar.this) {
+							MessageSectionFragment.this.archiveSnackbar = null;
+						}
+					}
+				});
+				this.snackbar.show();
+			} else {
+				this.snackbar = null;
+			}
+		}
+
+		void dismiss() {
+			if (this.snackbar != null) {
+				this.snackbar.dismiss();
+			}
+		}
+
+	}
 }

+ 6 - 2
app/src/main/java/ch/threema/app/fragments/wizard/WizardFragment4.java

@@ -22,19 +22,19 @@
 package ch.threema.app.fragments.wizard;
 
 import android.os.Bundle;
-import androidx.appcompat.widget.SwitchCompat;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.CompoundButton;
 
+import androidx.appcompat.widget.SwitchCompat;
 import ch.threema.app.R;
 import ch.threema.app.activities.wizard.WizardBaseActivity;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.SynchronizeContactsUtil;
 
 public class WizardFragment4 extends WizardFragment {
-	private static final boolean defaultSwitchValue = WizardBaseActivity.DEFAULT_SYNC_CONTACTS;
+	private static boolean defaultSwitchValue = WizardBaseActivity.DEFAULT_SYNC_CONTACTS;
 	private SwitchCompat syncContactsSwitch;
 	public static final int PAGE_ID = 4;
 
@@ -51,6 +51,10 @@ public class WizardFragment4 extends WizardFragment {
 
 		syncContactsSwitch = rootView.findViewById(R.id.wizard_switch_sync_contacts);
 
+		if (ConfigUtils.isOnPremBuild() && ConfigUtils.isDemoOPServer(preferenceService)) {
+			defaultSwitchValue = false;
+		}
+
 		if (SynchronizeContactsUtil.isRestrictedProfile(getActivity()) &&
 				!ConfigUtils.isWorkRestricted()) {
 			// restricted user profiles cannot add accounts

+ 2 - 13
app/src/main/java/ch/threema/app/jobs/ShareTargetShortcutUpdateJobService.java

@@ -27,9 +27,8 @@ import android.app.job.JobService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import ch.threema.app.ThreemaApplication;
 import ch.threema.app.backuprestore.csv.BackupService;
-import ch.threema.app.services.ShortcutService;
+import ch.threema.app.utils.ShortcutUtil;
 
 public class ShareTargetShortcutUpdateJobService extends JobService {
 	private static final Logger logger = LoggerFactory.getLogger(ShareTargetShortcutUpdateJobService.class);
@@ -44,17 +43,7 @@ public class ShareTargetShortcutUpdateJobService extends JobService {
 
 		new Thread(() -> {
 			logger.info("Updating share target shortcuts");
-
-			ShortcutService shortcutService;
-			try {
-				shortcutService = ThreemaApplication.getServiceManager().getShortcutService();
-				if (shortcutService != null) {
-					shortcutService.deleteAllShareTargetShortcuts();
-					shortcutService.publishRecentChatsAsShareTargets();
-				}
-			} catch (Exception e) {
-				logger.error("Exception, failed to update share target shortcuts", e);
-			}
+			ShortcutUtil.publishRecentChatsAsShareTargets();
 
 			jobFinished(jobParameters, false);
 		}, "ShareTargetShortcutUpdateJobService").start();

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

@@ -91,8 +91,6 @@ import ch.threema.app.services.SensorService;
 import ch.threema.app.services.SensorServiceImpl;
 import ch.threema.app.services.ServerAddressProviderService;
 import ch.threema.app.services.ServerAddressProviderServiceImpl;
-import ch.threema.app.services.ShortcutService;
-import ch.threema.app.services.ShortcutServiceImpl;
 import ch.threema.app.services.SynchronizeContactsService;
 import ch.threema.app.services.SynchronizeContactsServiceImpl;
 import ch.threema.app.services.SystemScreenLockService;
@@ -181,7 +179,6 @@ public class ServiceManager {
 	private NotificationService notificationService;
 	private SynchronizeContactsService synchronizeContactsService;
 	private SystemScreenLockService systemScreenLockService;
-	private ShortcutService shortcutService;
 
 	private IdListService blackListService, excludedSyncIdentitiesService, profilePicRecipientsService, readReceiptsRecipientsService, isTypingRecipientsService;
 	private DeadlineListService mutedChatsListService, hiddenChatListService, mentionOnlyChatsListService;
@@ -915,18 +912,6 @@ public class ServiceManager {
 		return this.systemScreenLockService;
 	}
 
-	public ShortcutService getShortcutService() throws ThreemaException {
-		if(this.shortcutService == null) {
-			this.shortcutService = new ShortcutServiceImpl(
-					this.getContext(),
-					this.getContactService(),
-					this.getConversationService(),
-					this.getPreferenceService()
-			);
-		}
-		return this.shortcutService;
-	}
-
 	public SensorService getSensorService() {
 		if (this.sensorService == null) {
 			this.sensorService = new SensorServiceImpl(this.getContext());

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

@@ -46,6 +46,7 @@ import ch.threema.app.stores.IdentityStore;
 import ch.threema.app.utils.NameUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.base.ThreemaException;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.domain.protocol.csp.messages.AbstractMessage;
 import ch.threema.domain.protocol.blob.BlobUploader;
 import ch.threema.domain.protocol.csp.messages.BoxLocationMessage;
@@ -73,7 +74,7 @@ import ch.threema.storage.models.data.MessageContentsType;
 import ch.threema.storage.models.data.media.FileDataModel;
 
 public class ContactMessageReceiver implements MessageReceiver<MessageModel> {
-	private static final Logger logger = LoggerFactory.getLogger(ContactMessageReceiver.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("ContactMessageReceiver");
 	private static final Logger validationLogger = LoggerFactory.getLogger("Validation");
 
 	private final ContactModel contactModel;

+ 2 - 2
app/src/main/java/ch/threema/app/messagereceiver/GroupMessageReceiver.java

@@ -530,7 +530,7 @@ public class GroupMessageReceiver implements MessageReceiver<GroupMessageModel>
 			}
 		}
 
-		this.groupMessagingService.sendMessage(this.group, groupIdentities, createApiMessage, queuedGroupMessage -> {
+		int enqueuedMessagesCount = this.groupMessagingService.sendMessage(this.group, groupIdentities, createApiMessage, queuedGroupMessage -> {
 			// Set as queued (first)
 			groupService.setIsArchived(group, false);
 
@@ -545,7 +545,7 @@ public class GroupMessageReceiver implements MessageReceiver<GroupMessageModel>
 					.create(
 							new GroupMessagePendingMessageIdModel(messageModel.getId(), queuedGroupMessage.getMessageId().toString()));
 		});
-		return true;
+		return enqueuedMessagesCount > 0;
 	}
 
 	@Override

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

@@ -122,13 +122,17 @@ public class SettingsAboutFragment extends ThreemaPreferenceFragment {
 			}
 		});
 
-		privacyPolicyPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
-			@Override
-			public boolean onPreferenceClick(Preference preference) {
-				startActivity(new Intent(getActivity().getApplicationContext(), PrivacyPolicyActivity.class));
-				return true;
-			}
-		});
+		if (ConfigUtils.isOnPremBuild() && !ConfigUtils.isDemoOPServer(preferenceService)) {
+			privacyPolicyPreference.setVisible(false);
+		} else {
+			privacyPolicyPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+				@Override
+				public boolean onPreferenceClick(Preference preference) {
+					startActivity(new Intent(getActivity().getApplicationContext(), PrivacyPolicyActivity.class));
+					return true;
+				}
+			});
+		}
 
 		if (ConfigUtils.isSerialLicensed() && !ConfigUtils.isWorkBuild()) {
 			checkUpdatePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {

+ 2 - 10
app/src/main/java/ch/threema/app/preference/SettingsPrivacyFragment.java

@@ -58,14 +58,13 @@ import ch.threema.app.managers.ListenerManager;
 import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.routines.SynchronizeContactsRoutine;
 import ch.threema.app.services.ContactService;
-import ch.threema.app.services.ShortcutService;
 import ch.threema.app.services.SynchronizeContactsService;
 import ch.threema.app.utils.AppRestrictionUtil;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.DialogUtil;
 import ch.threema.app.utils.RuntimeUtil;
+import ch.threema.app.utils.ShortcutUtil;
 import ch.threema.app.utils.SynchronizeContactsUtil;
-import ch.threema.base.ThreemaException;
 import ch.threema.localcrypto.MasterKeyLockedException;
 
 public class SettingsPrivacyFragment extends ThreemaPreferenceFragment implements CancelableHorizontalProgressDialog.ProgressDialogClickListener, GenericAlertDialog.DialogClickListener {
@@ -214,13 +213,6 @@ public class SettingsPrivacyFragment extends ThreemaPreferenceFragment implement
 			directSharePreference.setOnPreferenceChangeListener((preference, newValue) -> {
 				boolean newCheckedValue = newValue.equals(true);
 				if (((TwoStatePreference) preference).isChecked() != newCheckedValue) {
-					ShortcutService shortcutService = null;
-					try {
-						shortcutService = serviceManager.getShortcutService();
-					} catch (ThreemaException e) {
-						logger.error("Exception, could not update or delete shortcuts upon changing direct share setting", e);
-						return false;
-					}
 					if (newCheckedValue) {
 						ThreemaApplication.scheduleShareTargetShortcutUpdate();
 					} else {
@@ -228,7 +220,7 @@ public class SettingsPrivacyFragment extends ThreemaPreferenceFragment implement
 						if (jobScheduler != null) {
 							jobScheduler.cancel(ThreemaApplication.SHORTCUTS_UPDATE_JOB_ID);
 						}
-						shortcutService.deleteAllShareTargetShortcuts();
+						ShortcutUtil.deleteAllShareTargetShortcuts();
 					}
 				}
 				return true;

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

@@ -46,6 +46,7 @@ import ch.threema.app.services.group.IncomingGroupJoinRequestService;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.MessageDiskSizeUtil;
 import ch.threema.app.voip.services.VoipStateService;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.base.utils.Utils;
 import ch.threema.domain.models.MessageId;
 import ch.threema.domain.protocol.csp.ProtocolDefines;
@@ -85,7 +86,7 @@ import ch.threema.storage.models.MessageState;
 import ch.threema.storage.models.ServerMessageModel;
 
 public class MessageProcessor implements MessageProcessorInterface {
-	private static final Logger logger = LoggerFactory.getLogger(MessageProcessor.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("MessageProcessor");
 	private static final Logger validationLogger = LoggerFactory.getLogger("Validation");
 
 	private final MessageService messageService;

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

@@ -325,7 +325,7 @@ public class SynchronizeContactsRoutine implements Runnable {
 
 			if (contentProviderOperations.size() > 0) {
 				try {
-					context.getContentResolver().applyBatch(
+					ConfigUtils.applyToContentResolverInBatches(
 						ContactsContract.AUTHORITY,
 						contentProviderOperations);
 				} catch (Exception e) {

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

@@ -27,7 +27,6 @@ import android.content.Intent;
 import android.os.Build;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.lang.ref.WeakReference;
 
@@ -35,10 +34,11 @@ import ch.threema.app.ThreemaApplication;
 import ch.threema.app.activities.PinLockActivity;
 import ch.threema.app.utils.BiometricUtil;
 import ch.threema.app.utils.RuntimeUtil;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.localcrypto.MasterKey;
 
 public class ActivityService {
-	private static final Logger logger = LoggerFactory.getLogger(ActivityService.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("ActivityService");
 	private final Context context;
 	private final LockAppService lockAppService;
 	private final PreferenceService preferenceService;

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

@@ -29,7 +29,6 @@ import android.os.Bundle;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.util.Iterator;
 import java.util.Map;
@@ -39,6 +38,7 @@ import ch.threema.app.ThreemaApplication;
 import ch.threema.app.services.license.UserCredentials;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.base.ThreemaException;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.domain.protocol.api.APIConnector;
 import ch.threema.domain.protocol.api.work.WorkData;
 import ch.threema.domain.protocol.api.work.WorkMDMSettings;
@@ -47,7 +47,7 @@ import ch.threema.domain.protocol.api.work.WorkMDMSettings;
  * Hold all Work App Restrictions
  */
 public class AppRestrictionService {
-	private static final Logger logger = LoggerFactory.getLogger(AppRestrictionService.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("AppRestrictionService");
 
 	private Bundle appRestrictions;
 	private volatile WorkMDMSettings workMDMSettings;

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

@@ -30,7 +30,6 @@ import android.net.Uri;
 import android.util.LruCache;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.util.Collection;
 
@@ -45,12 +44,13 @@ import ch.threema.app.utils.AvatarConverterUtil;
 import ch.threema.app.utils.BitmapUtil;
 import ch.threema.app.utils.ColorUtil;
 import ch.threema.app.utils.ContactUtil;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.DistributionListModel;
 import ch.threema.storage.models.GroupModel;
 
 final public class AvatarCacheServiceImpl implements AvatarCacheService {
-	private static final Logger logger = LoggerFactory.getLogger(AvatarCacheServiceImpl.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("AvatarCacheServiceImpl");
 
 	private static final String KEY_GROUP = "g";
 	private static final String KEY_DISTRIBUTION_LIST = "d";

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

@@ -35,7 +35,6 @@ import com.neilalexander.jnacl.NaCl;
 import net.sqlcipher.Cursor;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.io.FileNotFoundException;
@@ -86,9 +85,11 @@ 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.ShortcutUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.base.ThreemaException;
 import ch.threema.base.utils.Base32;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.domain.models.IdentityType;
 import ch.threema.domain.models.VerificationLevel;
 import ch.threema.domain.protocol.ThreemaFeature;
@@ -111,7 +112,7 @@ import ch.threema.storage.models.ValidationMessage;
 import ch.threema.storage.models.access.AccessModel;
 
 public class ContactServiceImpl implements ContactService {
-	private static final Logger logger = LoggerFactory.getLogger(ContactServiceImpl.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("ContactServiceImpl");
 
 	private static final int TYPING_RECEIVE_TIMEOUT = (int) DateUtils.MINUTE_IN_MILLIS;
 	private static final String CONTACT_UID_PREFIX = "c-";
@@ -754,6 +755,9 @@ public class ContactServiceImpl implements ContactService {
 			this.profilePicRecipientsService.remove(model.getIdentity());
 			this.wallpaperService.removeWallpaper(uniqueIdString);
 			this.fileService.removeAndroidContactAvatar(model);
+			ShortcutUtil.deleteShareTargetShortcut(uniqueIdString);
+			ShortcutUtil.deletePinnedShortcut(uniqueIdString);
+
 		} else {
 			// hide contact
 			setIsHidden(model.getIdentity(),true);

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

@@ -24,7 +24,6 @@ package ch.threema.app.services;
 import net.sqlcipher.Cursor;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -43,6 +42,7 @@ import ch.threema.app.messagereceiver.GroupMessageReceiver;
 import ch.threema.app.messagereceiver.MessageReceiver;
 import ch.threema.app.utils.MessageUtil;
 import ch.threema.app.utils.TestUtil;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.storage.DatabaseServiceNew;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.ContactModel;
@@ -57,7 +57,7 @@ import ch.threema.storage.models.MessageType;
 import ch.threema.storage.models.TagModel;
 
 public class ConversationServiceImpl implements ConversationService {
-	private static final Logger logger = LoggerFactory.getLogger(ConversationServiceImpl.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("ConversationServiceImpl");
 
 	private final List<ConversationModel> conversationCache;
 	private final ConversationTagService conversationTagService;

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

@@ -38,6 +38,7 @@ import ch.threema.app.listeners.DistributionListListener;
 import ch.threema.app.managers.ListenerManager;
 import ch.threema.app.messagereceiver.DistributionListMessageReceiver;
 import ch.threema.app.utils.NameUtil;
+import ch.threema.app.utils.ShortcutUtil;
 import ch.threema.base.utils.Base32;
 import ch.threema.storage.DatabaseServiceNew;
 import ch.threema.storage.models.ContactModel;
@@ -191,6 +192,10 @@ public class DistributionListServiceImpl implements DistributionListService {
 		if(!this.removeMembers(distributionListModel)) {
 			return false;
 		}
+
+		ShortcutUtil.deleteShareTargetShortcut(getUniqueIdString(distributionListModel));
+		ShortcutUtil.deletePinnedShortcut(getUniqueIdString(distributionListModel));
+
 		//remove list
 		this.databaseServiceNew.getDistributionListModelFactory().delete(
 				distributionListModel

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

@@ -26,8 +26,7 @@ import androidx.annotation.WorkerThread;
 import ch.threema.base.ProgressListener;
 
 public interface DownloadService{
-	@WorkerThread @Nullable
-	byte[] download(int id, byte[] blobId, boolean markAsDown, ProgressListener progressListener);
+	@WorkerThread @Nullable byte[] download(int id, byte[] blobId, boolean markAsDown, @Nullable ProgressListener progressListener);
 	void complete(int id, byte[] blobId);
 	boolean cancel(int id);
 

+ 14 - 16
app/src/main/java/ch/threema/app/services/DownloadServiceImpl.java

@@ -28,7 +28,6 @@ import com.neilalexander.jnacl.NaCl;
 
 import org.apache.commons.io.IOUtils;
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.BufferedOutputStream;
 import java.io.File;
@@ -42,12 +41,13 @@ import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 import ch.threema.app.BuildConfig;
 import ch.threema.app.utils.FileUtil;
-import ch.threema.domain.protocol.blob.BlobLoader;
 import ch.threema.base.ProgressListener;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.base.utils.Utils;
+import ch.threema.domain.protocol.blob.BlobLoader;
 
 public class DownloadServiceImpl implements DownloadService {
-	private static final Logger logger = LoggerFactory.getLogger(DownloadServiceImpl.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("DownloadServiceImpl");
 
 	private static final String TAG = "DownloadService";
 	private static final String WAKELOCK_TAG = BuildConfig.APPLICATION_ID + ":" + TAG;
@@ -128,8 +128,7 @@ public class DownloadServiceImpl implements DownloadService {
 
 	@Override
 	@WorkerThread
-	@Nullable
-	public byte[] download(int messageModelId, final byte[] blobId, boolean markAsDown, ProgressListener progressListener) {
+	public @Nullable byte[] download(int messageModelId, final byte[] blobId, boolean markAsDown, @Nullable ProgressListener progressListener) {
 		PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG);
 		try {
 			if (wakeLock != null) {
@@ -142,10 +141,10 @@ public class DownloadServiceImpl implements DownloadService {
 				return null;
 			}
 
-			String blobIdHex = Utils.byteArrayToHexString(blobId);
+			final String blobIdHex = Utils.byteArrayToHexString(blobId);
 			logger.info("Blob {} for message {} download requested", blobIdHex, messageModelId);
 
-			byte[] imageBlob = null;
+			byte[] blobBytes = null;
 			File downloadFile = this.getTemporaryDownloadFile(blobId);
 			boolean downloadSuccess = false;
 
@@ -179,12 +178,11 @@ public class DownloadServiceImpl implements DownloadService {
 					blobLoader.setProgressListener(progressListener);
 				}
 
-
-				// load image from server
+				// Load blob from server
 				logger.info("Blob {} now fetching", blobIdHex);
-				imageBlob = blobLoader.load(false);
+				blobBytes = blobLoader.load(false);
 
-				if (imageBlob != null) {
+				if (blobBytes != null) {
 					synchronized (this.downloads) {
 						//check if loader already existing in array (otherwise its canceled)
 						if (getDownloadByBlobId(blobId) != null) {
@@ -193,11 +191,11 @@ public class DownloadServiceImpl implements DownloadService {
 							FileUtil.createNewFileOrLog(downloadFile, logger);
 							if (downloadFile.isFile()) {
 								try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(downloadFile))) {
-									bos.write(imageBlob);
+									bos.write(blobBytes);
 									bos.flush();
 								}
 
-								if (downloadFile.length() == imageBlob.length) {
+								if (downloadFile.length() == blobBytes.length) {
 									downloadSuccess = true;
 
 									//ok download saved, set as down if set
@@ -237,12 +235,12 @@ public class DownloadServiceImpl implements DownloadService {
 			}
 
 			if (downloadSuccess) {
-				logger.info("Blob {} successfully downloaded. Size = {}", blobIdHex, imageBlob.length);
+				logger.info("Blob {} successfully downloaded. Size = {}", blobIdHex, blobBytes.length);
 			} else {
 				logger.warn("Blob {} download failed.", blobIdHex);
 			}
 
-			if (imageBlob == null) {
+			if (blobBytes == null) {
 				synchronized (this.downloads) {
 					// download failed. remove loader
 					Download download = getDownloadByBlobId(blobId);
@@ -252,7 +250,7 @@ public class DownloadServiceImpl implements DownloadService {
 					}
 				}
 			}
-			return imageBlob;
+			return blobBytes;
 		} finally {
 			if (wakeLock != null && wakeLock.isHeld()) {
 				logger.info("Release download wakelock");

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

@@ -51,7 +51,6 @@ import org.apache.commons.io.IOUtils;
 import org.apache.commons.io.filefilter.AgeFileFilter;
 import org.apache.commons.io.filefilter.TrueFileFilter;
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.BufferedInputStream;
 import java.io.ByteArrayOutputStream;
@@ -108,6 +107,7 @@ import ch.threema.app.utils.SecureDeleteUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.base.ThreemaException;
 import ch.threema.base.utils.Base32;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.localcrypto.MasterKey;
 import ch.threema.localcrypto.MasterKeyLockedException;
 import ch.threema.storage.models.AbstractMessageModel;
@@ -119,7 +119,7 @@ import static android.provider.MediaStore.MEDIA_IGNORE_FILENAME;
 import static ch.threema.app.services.MessageServiceImpl.THUMBNAIL_SIZE_PX;
 
 public class FileServiceImpl implements FileService {
-	private static final Logger logger = LoggerFactory.getLogger(FileServiceImpl.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("FileServiceImpl");
 
 	private final static String JPEG_EXTENSION = ".jpg";
 	public final static String MPEG_EXTENSION = ".mp4";
@@ -1114,7 +1114,11 @@ public class FileServiceImpl implements FileService {
 
 		int preferredThumbnailWidth = ConfigUtils.getPreferredThumbnailWidth(context, false);
 		int maxWidth = THUMBNAIL_SIZE_PX << 1;
-		byte[] resizedThumbnailBytes = BitmapUtil.resizeBitmapByteArrayToMaxWidth(originalPicture, preferredThumbnailWidth > maxWidth ? maxWidth : preferredThumbnailWidth , pos, length);
+		byte[] resizedThumbnailBytes = BitmapUtil.resizeBitmapByteArrayToMaxWidth(originalPicture, Math.min(preferredThumbnailWidth, maxWidth), pos, length);
+		if (resizedThumbnailBytes == null) {
+			throw new Exception("Unable to scale thumbnail");
+		}
+
 		File thumbnailFile = this.getMessageThumbnail(messageModel);
 		if (thumbnailFile != null) {
 			FileUtil.createNewFileOrLog(thumbnailFile, logger);

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

@@ -22,7 +22,6 @@
 package ch.threema.app.services;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -33,12 +32,13 @@ import ch.threema.app.exceptions.EntryAlreadyExistsException;
 import ch.threema.app.exceptions.InvalidEntryException;
 import ch.threema.app.exceptions.PolicyViolationException;
 import ch.threema.base.ThreemaException;
-import ch.threema.domain.protocol.csp.messages.AbstractGroupMessage;
-import ch.threema.domain.protocol.csp.coders.MessageBox;
+import ch.threema.base.utils.LoggingUtil;
+import ch.threema.base.utils.Utils;
 import ch.threema.domain.models.GroupId;
 import ch.threema.domain.models.MessageId;
+import ch.threema.domain.protocol.csp.coders.MessageBox;
 import ch.threema.domain.protocol.csp.connection.MessageQueue;
-import ch.threema.base.utils.Utils;
+import ch.threema.domain.protocol.csp.messages.AbstractGroupMessage;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.GroupModel;
 import java8.util.J8Arrays;
@@ -49,7 +49,7 @@ import java8.util.stream.Stream;
  * {@inheritDoc}
  */
 public class GroupMessagingServiceImpl implements GroupMessagingService {
-	private static final Logger logger = LoggerFactory.getLogger(GroupMessagingServiceImpl.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("GroupMessagingServiceImpl");
 
 	private final UserService userService;
 	private final ContactService contactService;
@@ -167,7 +167,7 @@ public class GroupMessagingServiceImpl implements GroupMessagingService {
 			logger.debug("Sending group message {}", groupMessage);
 			final MessageBox messageBox = this.messageQueue.enqueue(groupMessage);
 			if (messageBox == null) {
-				logger.error("Failed to enqueue group message to {}", groupMessage.getToIdentity());
+				logger.info("Failed to enqueue group message to {}", groupMessage.getToIdentity());
 			} else {
 				enqueuedMessagesCount++;
 				if (logger.isDebugEnabled()) {

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

@@ -68,9 +68,11 @@ import ch.threema.app.messagereceiver.GroupMessageReceiver;
 import ch.threema.app.utils.AppRestrictionUtil;
 import ch.threema.app.utils.BitmapUtil;
 import ch.threema.app.utils.NameUtil;
+import ch.threema.app.utils.ShortcutUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.base.ThreemaException;
 import ch.threema.base.utils.Base32;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.base.utils.Utils;
 import ch.threema.domain.models.GroupId;
 import ch.threema.domain.models.IdentityState;
@@ -101,7 +103,7 @@ import ch.threema.storage.models.access.GroupAccessModel;
 import ch.threema.storage.models.group.GroupInviteModel;
 
 public class GroupServiceImpl implements GroupService {
-	private static final Logger logger = LoggerFactory.getLogger(GroupService.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("GroupServiceImpl");
 	private static final String GROUP_UID_PREFIX = "g-";
 
 	private final ApiService apiService;
@@ -252,6 +254,9 @@ public class GroupServiceImpl implements GroupService {
 
 		this.databaseServiceNew.getGroupMemberModelFactory().deleteByGroupId(groupModel.getId());
 
+		ShortcutUtil.deleteShareTargetShortcut(getUniqueIdString(groupModel));
+		ShortcutUtil.deletePinnedShortcut(getUniqueIdString(groupModel));
+
 		// save with "old" name
 		groupModel.setName(displayName);
 		this.save(groupModel);
@@ -324,6 +329,8 @@ public class GroupServiceImpl implements GroupService {
 		this.ringtoneService.removeCustomRingtone(uniqueIdString);
 		this.mutedChatsListService.remove(uniqueIdString);
 		this.hiddenChatsListService.remove(uniqueIdString);
+		ShortcutUtil.deleteShareTargetShortcut(uniqueIdString);
+		ShortcutUtil.deletePinnedShortcut(uniqueIdString);
 
 		groupModel.setDeleted(true);
 		this.databaseServiceNew.getGroupModelFactory().delete(

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

@@ -39,9 +39,10 @@ import ch.threema.app.ThreemaApplication;
 import ch.threema.app.backuprestore.csv.BackupService;
 import ch.threema.app.backuprestore.csv.RestoreService;
 import ch.threema.app.receivers.AlarmManagerBroadcastReceiver;
+import ch.threema.base.utils.LoggingUtil;
 
 public class LifetimeServiceImpl implements LifetimeService {
-	private static final Logger logger = LoggerFactory.getLogger(LifetimeServiceImpl.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("LifetimeServiceImpl");
 
 	public static final String REQUEST_CODE_KEY = "requestCode";
 	public static final int REQUEST_CODE_RELEASE = 1;

+ 60 - 62
app/src/main/java/ch/threema/app/services/MessageServiceImpl.java

@@ -433,6 +433,7 @@ public class MessageServiceImpl implements MessageService {
 				String messageId = messageModel.getApiMessageId();
 				logger.info(tag + ": message " + (messageId != null ? messageId : messageModel.getId()) + " successfully queued");
 			} else {
+				logger.info(tag + ": unable to send message. no recipients");
 				messageModel.setState(MessageState.SENDFAILED);
 			}
 			messageReceiver.saveLocalModel(messageModel);
@@ -1804,7 +1805,7 @@ public class MessageServiceImpl implements MessageService {
 
 		if(newModel) {
 			this.fireOnCreatedMessage(messageModel);
-
+			// Auto download
 			if (canDownload(messageModel)) {
 				downloadMediaMessage(messageModel, null);
 			}
@@ -2647,12 +2648,7 @@ public class MessageServiceImpl implements MessageService {
 			this.appLockService.addOnLockAppStateChanged(new LockAppService.OnLockAppStateChanged() {
 				@Override
 				public boolean changed(boolean locked) {
-					if(!locked) {
-//						fireOnNewMessage(messageModel);
-						return true;
-					}
-
-					return false;
+					return !locked;
 				}
 			});
 
@@ -2721,22 +2717,14 @@ public class MessageServiceImpl implements MessageService {
 				} else if (MimeUtil.isVideoFile(messageModel.getFileData().getMimeType())) {
 					if (TestUtil.empty(messageModel.getFileData().getCaption())) {
 						String durationString = messageModel.getFileData().getDurationString();
-						if (durationString != null) {
-							return new MessageString(prefix + context.getResources().getString(R.string.video_placeholder) + " (" + durationString + ")");
-						} else {
-							return new MessageString(prefix + context.getResources().getString(R.string.video_placeholder));
-						}
+						return new MessageString(prefix + context.getResources().getString(R.string.video_placeholder) + " (" + durationString + ")");
 					} else {
 						return new MessageString(prefix + context.getResources().getString(R.string.video_placeholder) + ": " + messageModel.getFileData().getCaption());
 					}
 				} else if (MimeUtil.isAudioFile(messageModel.getFileData().getMimeType())) {
 					if (TestUtil.empty(messageModel.getFileData().getCaption())) {
 						String durationString = messageModel.getFileData().getDurationString();
-						if (durationString != null) {
-							return new MessageString(prefix + context.getResources().getString(R.string.audio_placeholder) + " (" + durationString + ")");
-						} else {
-							return new MessageString(prefix + context.getResources().getString(R.string.audio_placeholder));
-						}
+						return new MessageString(prefix + context.getResources().getString(R.string.audio_placeholder) + " (" + durationString + ")");
 					} else {
 						return new MessageString(prefix + context.getResources().getString(R.string.audio_placeholder) + ": " + messageModel.getFileData().getCaption());
 					}
@@ -2807,76 +2795,84 @@ public class MessageServiceImpl implements MessageService {
 		}
 
 		if (data != null && !data.isDownloaded()) {
+			boolean success = false;
 
-			byte[] blob = this.downloadService.download(
+			if (mediaMessageModel.getType() != MessageType.IMAGE) {
+				File messageFile = this.fileService.getMessageFile(mediaMessageModel);
+				if (messageFile != null && messageFile.exists() && messageFile.length() > NaCl.BOXOVERHEAD) {
+					// hack: do not re-download a blob that's already present on the file system
+					success = true;
+				}
+			}
+
+			if (!success) {
+				byte[] blob = this.downloadService.download(
 					mediaMessageModel.getId(),
 					data.getBlobId(),
 					!(mediaMessageModel instanceof GroupMessageModel),
 					progressListener);
-			if (blob == null || blob.length < NaCl.BOXOVERHEAD) {
-				logger.error("Blob for message {} is empty", mediaMessageModel.getApiMessageId());
+				if (blob == null || blob.length < NaCl.BOXOVERHEAD) {
+					logger.error("Blob for message {} is empty", mediaMessageModel.getApiMessageId());
 
-				this.downloadService.error(mediaMessageModel.getId());
-				// blob download failed or empty or canceled
-				throw new ThreemaException("failed to download message");
-			}
+					this.downloadService.error(mediaMessageModel.getId());
+					// blob download failed or empty or canceled
+					throw new ThreemaException("failed to download message");
+				}
 
-			boolean success = false;
-			if (mediaMessageModel.getType() != MessageType.IMAGE) {
-				logger.info("Decrypting blob for message {}", mediaMessageModel.getApiMessageId());
+				if (mediaMessageModel.getType() != MessageType.IMAGE) {
+					logger.info("Decrypting blob for message {}", mediaMessageModel.getApiMessageId());
 
-				if (NaCl.symmetricDecryptDataInplace(blob, data.getEncryptionKey(), nonce)) {
-					logger.info("Write conversation media for message {}", mediaMessageModel.getApiMessageId());
+					if (NaCl.symmetricDecryptDataInplace(blob, data.getEncryptionKey(), nonce)) {
+						logger.info("Write conversation media for message {}", mediaMessageModel.getApiMessageId());
 
-					// save the file
-					try {
-						if (this.fileService.writeConversationMedia(mediaMessageModel, blob, 0, blob.length - NaCl.BOXOVERHEAD, true)) {
-							success = true;
-							logger.info("Media for message {} successfully saved.", mediaMessageModel.getApiMessageId());
-						}
-					} catch (Exception e) {
-						logger.warn("Unable to save media");
+						// save the file
+						try {
+							if (this.fileService.writeConversationMedia(mediaMessageModel, blob, 0, blob.length - NaCl.BOXOVERHEAD, true)) {
+								success = true;
+								logger.info("Media for message {} successfully saved.", mediaMessageModel.getApiMessageId());
+							}
+						} catch (Exception e) {
+							logger.warn("Unable to save media");
 
-						this.downloadService.error(mediaMessageModel.getId());
+							this.downloadService.error(mediaMessageModel.getId());
 
-						throw new ThreemaException("Unable to save media");
+							throw new ThreemaException("Unable to save media");
+						}
 					}
-				}
-			} else {
-				byte[] image;
-
-				if (mediaMessageModel instanceof GroupMessageModel) {
-					image = NaCl.symmetricDecryptData(blob, data.getEncryptionKey(), nonce);
 				} else {
-					image = identityStore.decryptData(blob, data.getNonce(), data.getEncryptionKey());
-				}
+					byte[] image;
 
-				if (image != null && image.length > 0) {
-					try {
-						// save the file
-						success = saveStrippedImage(image, mediaMessageModel);
-					} catch (Exception e) {
-						logger.error("Exception", e);
+					if (mediaMessageModel instanceof GroupMessageModel) {
+						image = NaCl.symmetricDecryptData(blob, data.getEncryptionKey(), nonce);
+					} else {
+						image = identityStore.decryptData(blob, data.getNonce(), data.getEncryptionKey());
+					}
+
+					if (image != null && image.length > 0) {
+						try {
+							// save the file
+							success = saveStrippedImage(image, mediaMessageModel);
+						} catch (Exception e) {
+							logger.error("Exception", e);
+						}
 					}
 				}
 			}
 
 			if (success) {
-				data.isDownloaded(true);
-
 				if(mediaMessageModel.getType() == MessageType.IMAGE) {
-					mediaMessageModel.setImageData((ImageDataModel)data);
-					mediaMessageModel.writeDataModelToBody();
+					mediaMessageModel.getImageData().isDownloaded(true);
 				}
 				else if(mediaMessageModel.getType() == MessageType.VIDEO) {
-					mediaMessageModel.setVideoData((VideoDataModel)data);
+					mediaMessageModel.getVideoData().isDownloaded(true);
 				}
 				else if(mediaMessageModel.getType() == MessageType.VOICEMESSAGE) {
-					mediaMessageModel.setAudioData((AudioDataModel) data);
+					mediaMessageModel.getAudioData().isDownloaded(true);
 				}
 				else if(mediaMessageModel.getType() == MessageType.FILE) {
-					mediaMessageModel.setFileData((FileDataModel) data);
+					mediaMessageModel.getFileData().isDownloaded(true);
 				}
+				mediaMessageModel.writeDataModelToBody();
 
 				this.save(mediaMessageModel);
 
@@ -3145,6 +3141,8 @@ public class MessageServiceImpl implements MessageService {
 
 				try {
 					context.startActivity(Intent.createChooser(intent, context.getResources().getText(R.string.share_via)));
+
+					return true;
 				} catch (ActivityNotFoundException e) {
 					// make sure Toast runs in UI thread
 					RuntimeUtil.runOnUiThread(new Runnable() {
@@ -3876,7 +3874,7 @@ public class MessageServiceImpl implements MessageService {
 						mediaItem.setDurationMs(trimmedDuration);
 					}
 				}
-				metaData.put(FileDataModel.METADATA_KEY_DURATION, trimmedDuration / (float) DateUtils.SECOND_IN_MILLIS);
+				metaData.put(FileDataModel.METADATA_KEY_DURATION, (float) trimmedDuration / (float) DateUtils.SECOND_IN_MILLIS);
 				thumbnailBitmap = IconUtil.getVideoThumbnailFromUri(context, mediaItem);
 				fileDataModel.setThumbnailMimeType(MimeUtil.MIME_TYPE_IMAGE_JPG);
 				break;
@@ -3915,7 +3913,7 @@ public class MessageServiceImpl implements MessageService {
 				thumbnailBitmap = IconUtil.getThumbnailFromUri(context, mediaItem.getUri(), THUMBNAIL_SIZE_PX, fileDataModel.getMimeType(), true);
 				break;
 			case MediaItem.TYPE_VOICEMESSAGE:
-				metaData.put(FileDataModel.METADATA_KEY_DURATION, mediaItem.getDurationMs() / (float) DateUtils.SECOND_IN_MILLIS);
+				metaData.put(FileDataModel.METADATA_KEY_DURATION, (float) mediaItem.getDurationMs() / (float) DateUtils.SECOND_IN_MILLIS);
 				// voice messages do not have thumbnails
 				thumbnailBitmap = null;
 				break;

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

@@ -27,9 +27,9 @@ import java.net.URL;
 import java.net.URLEncoder;
 
 import ch.threema.app.BuildConfig;
-import ch.threema.domain.onprem.OnPremConfigFetcher;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.base.ThreemaException;
+import ch.threema.domain.onprem.OnPremConfigFetcher;
 import ch.threema.domain.onprem.ServerAddressProviderOnPrem;
 import ch.threema.domain.protocol.ServerAddressProvider;
 
@@ -125,6 +125,11 @@ public class ServerAddressProviderServiceImpl implements ServerAddressProviderSe
 			public String getSafeServerUrl(boolean ipv6) throws ThreemaException {
 				return BuildConfig.SAFE_SERVER_URL;
 			}
+
+			@Override
+			public String getWebServerUrl() throws ThreemaException {
+				return BuildConfig.WEB_SERVER_URL;
+			}
 		};
 	}
 

+ 0 - 46
app/src/main/java/ch/threema/app/services/ShortcutService.java

@@ -1,46 +0,0 @@
-/*  _____ _
- * |_   _| |_  _ _ ___ ___ _ __  __ _
- *   | | | ' \| '_/ -_) -_) '  \/ _` |_
- *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
- *
- * Threema for Android
- * Copyright (c) 2017-2022 Threema GmbH
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package ch.threema.app.services;
-
-import android.os.BaseBundle;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.WorkerThread;
-import ch.threema.app.messagereceiver.MessageReceiver;
-import ch.threema.storage.models.AbstractMessageModel;
-
-public interface ShortcutService {
-	int TYPE_NONE = 0;
-	int TYPE_CHAT = 1;
-	int TYPE_CALL = 2;
-
-	/* pinned shortcuts */
-	@WorkerThread void createPinnedShortcut(MessageReceiver<? extends AbstractMessageModel> messageReceiver, int type);
-	@WorkerThread void updatePinnedShortcut(MessageReceiver<? extends AbstractMessageModel> messageReceiver);
-	@WorkerThread void deletePinnedShortcut(MessageReceiver<? extends AbstractMessageModel> messageReceiver);
-
-	/* dynamic shortcuts (share targets) */
-	@WorkerThread void publishRecentChatsAsShareTargets();
-	@WorkerThread void deleteAllShareTargetShortcuts();
-	@Nullable BaseBundle getShareTargetExtrasFromShortcutId(@NonNull String id);
-}

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

@@ -224,7 +224,7 @@ public class AudioMessagePlayer extends MessagePlayer implements AudioManager.On
 			if (d instanceof AudioDataModel) {
 				duration = ((AudioDataModel) d).getDuration();
 			} else if (d instanceof FileDataModel) {
-				duration = (int) ((FileDataModel) d).getDuration();
+				duration = (int) ((FileDataModel) d).getDurationSeconds();
 			}
 		}
 		logger.debug("duration = {}", duration);

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

@@ -559,9 +559,7 @@ public class AndroidContactUtil {
 		int operationCount = contentProviderOperations.size();
 		if (operationCount > 0) {
 			try {
-				ThreemaApplication.getAppContext().getContentResolver().applyBatch(
-					ContactsContract.AUTHORITY,
-					contentProviderOperations);
+				ConfigUtils.applyToContentResolverInBatches(ContactsContract.AUTHORITY, contentProviderOperations);
 			} catch (Exception e) {
 				logger.error("Error during raw contact deletion! ", e);
 			}

+ 10 - 9
app/src/main/java/ch/threema/app/utils/BitmapUtil.java

@@ -48,7 +48,6 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.lang.ref.WeakReference;
 
 import androidx.annotation.ColorInt;
 import androidx.annotation.IntDef;
@@ -368,8 +367,9 @@ public class BitmapUtil {
 	 * @param maxWidth maximum width of the image after scaling is applied
 	 * @param pos offset withing byte array
 	 * @param length the number of bytes, beginning at offset, to parse
-	 * @return compressed byte array of scaled bitmap in either PNG or JPG format - depending on source bitmap format
+	 * @return compressed byte array of scaled bitmap in either PNG or JPG format - depending on source bitmap format - or null in case of an error
 	 */
+	@Nullable
 	static public byte[] resizeBitmapByteArrayToMaxWidth(byte[] sourceBitmapFileBytes, int maxWidth, int pos, int length) {
 		try {
 			boolean isJpeg = ExifInterface.isJpegFormat(sourceBitmapFileBytes);
@@ -379,17 +379,18 @@ public class BitmapUtil {
 			o2.inScaled = true;
 			o2.inPreferredConfig = isJpeg ? Bitmap.Config.RGB_565 : Bitmap.Config.ARGB_8888;
 
-			WeakReference<Bitmap> newPhoto = new WeakReference<>(BitmapFactory.decodeByteArray(sourceBitmapFileBytes, pos, length, o2));
-
-			if (isJpeg) {
-				return bitmapToJpegByteArray(newPhoto.get());
-			} else {
-				return bitmapToPngByteArray(newPhoto.get());
+			Bitmap newPhoto = BitmapFactory.decodeByteArray(sourceBitmapFileBytes, pos, length, o2);
+			if (newPhoto != null) {
+				if (isJpeg) {
+					return bitmapToJpegByteArray(newPhoto);
+				} else {
+					return bitmapToPngByteArray(newPhoto);
+				}
 			}
 		} catch (Exception x) {
 			logger.error("Exception", x);
-			return null;
 		}
+		return null;
 	}
 
 	/**

+ 27 - 0
app/src/main/java/ch/threema/app/utils/ConfigUtils.java

@@ -28,8 +28,11 @@ import android.app.AlarmManager;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.OperationApplicationException;
 import android.content.SharedPreferences;
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageInfo;
@@ -44,6 +47,7 @@ import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.RemoteException;
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.DisplayMetrics;
@@ -68,6 +72,8 @@ import org.slf4j.LoggerFactory;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Locale;
 
 import javax.net.ssl.SSLSocketFactory;
@@ -124,6 +130,7 @@ public class ConfigUtils {
 	public static final int THEME_DARK = 1;
 	public static final int THEME_SYSTEM = 2;
 	public static final int THEME_NONE = -1;
+	private static final int CONTENT_PROVIDER_BATCH_SIZE = 50;
 
 	@Retention(RetentionPolicy.SOURCE)
 	@IntDef({THEME_LIGHT, THEME_DARK})
@@ -583,6 +590,10 @@ public class ConfigUtils {
 		return BuildFlavor.getLicenseType().equals(BuildFlavor.LicenseType.ONPREM);
 	}
 
+	public static boolean isDemoOPServer(@NonNull PreferenceService preferenceService) {
+		return preferenceService.getOnPremServer() != null && preferenceService.getOnPremServer().toLowerCase().contains(".3ma.ch/");
+	}
+
 	public static boolean isTestBuild() {
 		return BuildFlavor.getName().contains("DEBUG") ||
 			BuildFlavor.getName().equals("Red") || BuildFlavor.getName().equals("DEV") ||
@@ -1374,4 +1385,20 @@ public class ConfigUtils {
 			}
 		} catch (Exception ignored) {}
 	}
+
+	/**
+	 * Apply operations to content provider in small batches preventing TransactionTooLargeException
+	 * @param authority Authority
+	 * @param contentProviderOperations Operations to apply in smaller batches
+	 * @throws OperationApplicationException
+	 * @throws RemoteException
+	 */
+	public static void applyToContentResolverInBatches(@NonNull String authority, ArrayList<ContentProviderOperation> contentProviderOperations) throws OperationApplicationException, RemoteException {
+		ContentResolver contentResolver = ThreemaApplication.getAppContext().getContentResolver();
+
+		for (int i = 0; i < contentProviderOperations.size(); i += CONTENT_PROVIDER_BATCH_SIZE) {
+			List<ContentProviderOperation> contentProviderOperationsBatch = contentProviderOperations.subList(i, Math.min(contentProviderOperations.size(), i + CONTENT_PROVIDER_BATCH_SIZE));
+			contentResolver.applyBatch(authority, new ArrayList<>(contentProviderOperationsBatch));
+		}
+	}
 }

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

@@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory;
 import java.util.Date;
 import java.util.HashMap;
 
+import androidx.annotation.Nullable;
 import androidx.core.app.Person;
 import androidx.core.graphics.drawable.IconCompat;
 import ch.threema.app.R;
@@ -86,7 +87,7 @@ public class ConversationNotificationUtil {
 		}
 	}
 
-	private static Person getSenderPerson(AbstractMessageModel messageModel) {
+	private static @Nullable Person getSenderPerson(AbstractMessageModel messageModel) {
 		//load lazy
 		try {
 			final ContactService contactService = ThreemaApplication.getServiceManager().getContactService();
@@ -99,7 +100,11 @@ public class ConversationNotificationUtil {
 		}
 	}
 
-	public static Person getPerson(ContactService contactService, ContactModel contactModel, String name) {
+	public static @Nullable Person getPerson(@Nullable ContactService contactService, ContactModel contactModel, String name) {
+		if (contactService == null) {
+			return null;
+		}
+
 		Person.Builder builder = new Person.Builder()
 			.setKey(contactService.getUniqueIdString(contactModel))
 			.setName(name);

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

@@ -465,8 +465,8 @@ public class MessageUtil {
 						if (messageModel.getFileData().getRenderingType() == FileData.RENDERING_MEDIA) {
 							return new MessageViewElement(R.drawable.ic_mic_filled,
 								context.getString(R.string.audio_placeholder),
-								StringConversionUtil.secondsToString(messageModel.getFileData().getDuration(), false),
-								". " + context.getString(R.string.duration) + " " + StringConversionUtil.getDurationStringHuman(context, messageModel.getFileData().getDuration()) + ". ",
+								StringConversionUtil.secondsToString(messageModel.getFileData().getDurationSeconds(), false),
+								". " + context.getString(R.string.duration) + " " + StringConversionUtil.getDurationStringHuman(context, messageModel.getFileData().getDurationSeconds()) + ". ",
 								null);
 						} else {
 							return new MessageViewElement(R.drawable.ic_doc_audio,

+ 147 - 88
app/src/main/java/ch/threema/app/services/ShortcutServiceImpl.java → app/src/main/java/ch/threema/app/utils/ShortcutUtil.java

@@ -4,7 +4,7 @@
  *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
  *
  * Threema for Android
- * Copyright (c) 2017-2022 Threema GmbH
+ * Copyright (c) 2015-2022 Threema GmbH
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License, version 3,
@@ -19,7 +19,7 @@
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
  */
 
-package ch.threema.app.services;
+package ch.threema.app.utils;
 
 import android.content.ComponentName;
 import android.content.Context;
@@ -36,8 +36,10 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -57,41 +59,34 @@ import ch.threema.app.messagereceiver.ContactMessageReceiver;
 import ch.threema.app.messagereceiver.DistributionListMessageReceiver;
 import ch.threema.app.messagereceiver.GroupMessageReceiver;
 import ch.threema.app.messagereceiver.MessageReceiver;
-import ch.threema.app.utils.AvatarConverterUtil;
-import ch.threema.app.utils.BitmapUtil;
-import ch.threema.app.utils.ConversationNotificationUtil;
-import ch.threema.app.utils.IntentDataUtil;
-import ch.threema.app.utils.TestUtil;
+import ch.threema.app.services.ContactService;
+import ch.threema.app.services.ConversationService;
 import ch.threema.app.voip.activities.CallActivity;
 import ch.threema.app.voip.services.VoipCallService;
+import ch.threema.base.ThreemaException;
 import ch.threema.storage.models.AbstractMessageModel;
+import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.ConversationModel;
 
 import static androidx.core.content.pm.ShortcutManagerCompat.FLAG_MATCH_PINNED;
 
-public class ShortcutServiceImpl implements ShortcutService {
-	private static final Logger logger = LoggerFactory.getLogger(ShortcutService.class);
-	private static final int MAX_SHARE_TARGETS = 4; // we recommend that you publish only four distinct shortcuts to improve their visual appearance in the launcher. https://developer.android.com/guide/topics/ui/shortcuts/best-practices
+public final class ShortcutUtil {
+	private static final Logger logger = LoggerFactory.getLogger(ShortcutUtil.class);
 
-	private final Context context;
-	private final ContactService contactService;
-	private final ConversationService conversationService;
-	private final PreferenceService preferenceService;
+	private static final int MAX_SHARE_TARGETS = 100; // we recommend that you publish only four distinct shortcuts to improve their visual appearance in the launcher. https://developer.android.com/guide/topics/ui/shortcuts/best-practices
 
-	private final Object dynamicShortcutLock = new Object();
+	public static final int TYPE_NONE = 0;
+	public static final int TYPE_CHAT = 1;
+	public static final int TYPE_CALL = 2;
 
-	private static final String DYNAMIC_SHORTCUT_SHARE_TARGET_CATEGORY = "ch.threema.app.category.DYNAMIC_SHORTCUT_SHARE_TARGET"; // do not use BuildConfig.APPLICATION_ID
+	private static final Object dynamicShortcutLock = new Object();
 
-	public ShortcutServiceImpl(Context context, ContactService contactService, ConversationService conversationService, PreferenceService preferenceService) {
-		this.context = context;
-		this.contactService = contactService;
-		this.conversationService = conversationService;
-		this.preferenceService = preferenceService;
-	}
+	private static final String DYNAMIC_SHORTCUT_SHARE_TARGET_CATEGORY = "ch.threema.app.category.DYNAMIC_SHORTCUT_SHARE_TARGET"; // do not use BuildConfig.APPLICATION_ID
 
 	private static class CommonShortcutInfo {
 		Intent intent;
-		@Nullable Bitmap bitmap;
+		@Nullable
+		Bitmap bitmap;
 		String longLabel;
 		String shortLabel;
 		String uniqueId;
@@ -99,30 +94,28 @@ public class ShortcutServiceImpl implements ShortcutService {
 
 	/*****************************************************************************************************************/
 
-	@Override
 	@WorkerThread
-	public void createPinnedShortcut(MessageReceiver<? extends AbstractMessageModel> messageReceiver, int type) {
+	public static void createPinnedShortcut(MessageReceiver<? extends AbstractMessageModel> messageReceiver, int type) {
 		ShortcutInfoCompat shortcutInfoCompat = getPinnedShortcutInfo(messageReceiver, type);
 
 		if (shortcutInfoCompat != null) {
-			if (ShortcutManagerCompat.requestPinShortcut(context, shortcutInfoCompat, null)) {
-				Toast.makeText(context, R.string.add_shortcut_success, Toast.LENGTH_SHORT).show();
+			if (ShortcutManagerCompat.requestPinShortcut(getContext(), shortcutInfoCompat, null)) {
+				Toast.makeText(getContext(), R.string.add_shortcut_success, Toast.LENGTH_SHORT).show();
 			} else {
-				Toast.makeText(context, R.string.add_shortcut_error, Toast.LENGTH_SHORT).show();
+				Toast.makeText(getContext(), R.string.add_shortcut_error, Toast.LENGTH_SHORT).show();
 				logger.info("Failed to add shortcut");
 			}
 		}
 	}
 
-	@Override
 	@WorkerThread
-	public void updatePinnedShortcut(MessageReceiver<? extends AbstractMessageModel> messageReceiver) {
+	public static void updatePinnedShortcut(MessageReceiver<? extends AbstractMessageModel> messageReceiver) {
 		String uniqueId = messageReceiver.getUniqueIdString();
 
 		if (!TestUtil.empty(uniqueId)) {
 			List<ShortcutInfoCompat> matchingShortcuts = new ArrayList<>();
 
-			for (ShortcutInfoCompat shortcutInfo : ShortcutManagerCompat.getShortcuts(context, FLAG_MATCH_PINNED)) {
+			for (ShortcutInfoCompat shortcutInfo : ShortcutManagerCompat.getShortcuts(getContext(), FLAG_MATCH_PINNED)) {
 				if (shortcutInfo.getId().equals(TYPE_CHAT + uniqueId)) {
 					matchingShortcuts.add(getPinnedShortcutInfo(messageReceiver, TYPE_CHAT));
 				} else if (shortcutInfo.getId().equals(TYPE_CALL + uniqueId)) {
@@ -131,26 +124,23 @@ public class ShortcutServiceImpl implements ShortcutService {
 			}
 
 			if (matchingShortcuts.size() > 0) {
-				ShortcutManagerCompat.updateShortcuts(context, matchingShortcuts);
+				ShortcutManagerCompat.updateShortcuts(getContext(), matchingShortcuts);
 			}
 		}
 	}
 
-	@Override
 	@WorkerThread
-	public void deletePinnedShortcut(MessageReceiver<? extends AbstractMessageModel> messageReceiver) {
-		String uniqueId = messageReceiver.getUniqueIdString();
-
-		if (!TestUtil.empty(uniqueId)) {
-			List<ShortcutInfoCompat> shortcutInfos = ShortcutManagerCompat.getDynamicShortcuts(context);
+	public static void deletePinnedShortcut(String uniqueIdString) {
+		if (!TestUtil.empty(uniqueIdString)) {
+			List<ShortcutInfoCompat> shortcutInfos = ShortcutManagerCompat.getShortcuts(getContext(), FLAG_MATCH_PINNED);
 
 			if (shortcutInfos.size() > 0) {
 				for (ShortcutInfoCompat shortcutInfo : shortcutInfos) {
 					String shortcutId = shortcutInfo.getId();
 					if (!TestUtil.empty(shortcutId)) {
 						// ignore first character which represents the type indicator
-						if (shortcutId.substring(1).equals(uniqueId)) {
-							ShortcutManagerCompat.removeLongLivedShortcuts(context, Collections.singletonList(shortcutInfo.getId()));
+						if (shortcutId.substring(1).equals(uniqueIdString)) {
+							ShortcutManagerCompat.removeLongLivedShortcuts(getContext(), Collections.singletonList(shortcutInfo.getId()));
 							break;
 						}
 					}
@@ -159,8 +149,29 @@ public class ShortcutServiceImpl implements ShortcutService {
 		}
 	}
 
+	/**
+	 * Delete all pinned shortcuts associated with our app
+	 */
+	@WorkerThread
+	public static void deleteAllPinnedShortcuts() {
+		List<ShortcutInfoCompat> shortcutInfos = ShortcutManagerCompat.getShortcuts(getContext(), FLAG_MATCH_PINNED);
+
+		if (shortcutInfos.size() > 0) {
+			List<String> shortcutIds = new ArrayList<>();
+
+			for (ShortcutInfoCompat shortcutInfoCompat : shortcutInfos) {
+				shortcutIds.add(shortcutInfoCompat.getId());
+			}
+			try {
+				ShortcutManagerCompat.removeLongLivedShortcuts(getContext(), shortcutIds);
+			} catch (IllegalStateException e) {
+				logger.error("Failed to remove shortcuts.", e);
+			}
+		}
+	}
+
 	@NonNull
-	private CommonShortcutInfo getCommonShortcutInfo(@NonNull MessageReceiver<? extends AbstractMessageModel> messageReceiver, int type) {
+	private static CommonShortcutInfo getCommonShortcutInfo(@NonNull MessageReceiver<? extends AbstractMessageModel> messageReceiver, int type) {
 		CommonShortcutInfo commonShortcutInfo = new CommonShortcutInfo();
 
 		Bitmap bitmap = messageReceiver.getNotificationAvatar();
@@ -172,14 +183,14 @@ public class ShortcutServiceImpl implements ShortcutService {
 				// backwards compatibility
 				commonShortcutInfo.intent.putExtra(VoipCallService.EXTRA_CONTACT_IDENTITY, ((ContactMessageReceiver) messageReceiver).getContact().getIdentity());
 			}
-			commonShortcutInfo.longLabel = String.format(context.getString(R.string.threema_call_with), messageReceiver.getDisplayName());
-			VectorDrawableCompat phoneDrawable = VectorDrawableCompat.create(context.getResources(), R.drawable.ic_phone_locked, context.getTheme());
-			Bitmap phoneBitmap = AvatarConverterUtil.getAvatarBitmap(phoneDrawable, Color.BLACK, context.getResources().getDimensionPixelSize(R.dimen.shortcut_overlay_size));
-			commonShortcutInfo.bitmap = bitmap != null ? BitmapUtil.addOverlay(bitmap, phoneBitmap, context.getResources().getDimensionPixelSize(R.dimen.call_shortcut_shadow_offset)) : null;
+			commonShortcutInfo.longLabel = String.format(getContext().getString(R.string.threema_call_with), messageReceiver.getDisplayName());
+			VectorDrawableCompat phoneDrawable = VectorDrawableCompat.create(getContext().getResources(), R.drawable.ic_phone_locked, getContext().getTheme());
+			Bitmap phoneBitmap = AvatarConverterUtil.getAvatarBitmap(phoneDrawable, Color.BLACK, getContext().getResources().getDimensionPixelSize(R.dimen.shortcut_overlay_size));
+			commonShortcutInfo.bitmap = bitmap != null ? BitmapUtil.addOverlay(bitmap, phoneBitmap, getContext().getResources().getDimensionPixelSize(R.dimen.call_shortcut_shadow_offset)) : null;
 		} else {
 			commonShortcutInfo.intent = getChatShortcutIntent();
 			IntentDataUtil.addMessageReceiverToIntent(commonShortcutInfo.intent, messageReceiver);
-			commonShortcutInfo.longLabel = String.format(context.getString(R.string.chat_with), messageReceiver.getDisplayName());
+			commonShortcutInfo.longLabel = String.format(getContext().getString(R.string.chat_with), messageReceiver.getDisplayName());
 			commonShortcutInfo.bitmap = bitmap;
 		}
 		commonShortcutInfo.shortLabel = messageReceiver.getShortName();
@@ -189,16 +200,16 @@ public class ShortcutServiceImpl implements ShortcutService {
 	}
 
 	@Nullable
-	public ShortcutInfoCompat getPinnedShortcutInfo(MessageReceiver<? extends AbstractMessageModel> messageReceiver, int type) {
+	public static ShortcutInfoCompat getPinnedShortcutInfo(MessageReceiver<? extends AbstractMessageModel> messageReceiver, int type) {
 		CommonShortcutInfo commonShortcutInfo = getCommonShortcutInfo(messageReceiver, type);
 
 		try {
 			Person person = null;
 			if (messageReceiver instanceof ContactMessageReceiver) {
-				person = ConversationNotificationUtil.getPerson(contactService, ((ContactMessageReceiver) messageReceiver).getContact(), messageReceiver.getDisplayName());
+				person = ConversationNotificationUtil.getPerson(getContactService(), ((ContactMessageReceiver) messageReceiver).getContact(), messageReceiver.getDisplayName());
 			}
 
-			ShortcutInfoCompat.Builder shortcutInfoCompatBuilder = new ShortcutInfoCompat.Builder(context, type + commonShortcutInfo.uniqueId)
+			ShortcutInfoCompat.Builder shortcutInfoCompatBuilder = new ShortcutInfoCompat.Builder(getContext(), type + commonShortcutInfo.uniqueId)
 				.setShortLabel(commonShortcutInfo.shortLabel)
 				.setLongLabel(commonShortcutInfo.longLabel)
 				.setIntent(commonShortcutInfo.intent)
@@ -219,8 +230,8 @@ public class ShortcutServiceImpl implements ShortcutService {
 		return null;
 	}
 
-	private Intent getChatShortcutIntent() {
-		Intent intent = new Intent(context, ComposeMessageActivity.class);
+	private static Intent getChatShortcutIntent() {
+		Intent intent = new Intent(getContext(), ComposeMessageActivity.class);
 		intent.setData((Uri.parse("foobar://" + SystemClock.elapsedRealtime())));
 		intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
 		intent.setAction(Intent.ACTION_MAIN);
@@ -228,8 +239,8 @@ public class ShortcutServiceImpl implements ShortcutService {
 		return intent;
 	}
 
-	private Intent getCallShortcutIntent() {
-		Intent intent = new Intent(context, CallActivity.class);
+	private static Intent getCallShortcutIntent() {
+		Intent intent = new Intent(getContext(), CallActivity.class);
 		intent.setData((Uri.parse("foobar://" + SystemClock.elapsedRealtime())));
 		intent.setAction(Intent.ACTION_MAIN);
 		intent.putExtra(CallActivity.EXTRA_CALL_FROM_SHORTCUT, true);
@@ -239,8 +250,8 @@ public class ShortcutServiceImpl implements ShortcutService {
 		return intent;
 	}
 
-	private Intent getShareTargetShortcutIntent(MessageReceiver<? extends AbstractMessageModel> messageReceiver) {
-		Intent intent = new Intent(context, RecipientListActivity.class);
+	private static Intent getShareTargetShortcutIntent(MessageReceiver<? extends AbstractMessageModel> messageReceiver) {
+		Intent intent = new Intent(getContext(), RecipientListActivity.class);
 		intent.setData((Uri.parse("foobar://" + SystemClock.elapsedRealtime())));
 		intent.setAction(Intent.ACTION_DEFAULT);
 		IntentDataUtil.addMessageReceiverToIntent(intent, messageReceiver);
@@ -254,44 +265,58 @@ public class ShortcutServiceImpl implements ShortcutService {
 	 * Try publishing the most recent chats as dynamic shortcuts to be shown as share targets or in a launcher popup
 	 * You may want to call deleteAllShareTargetShortcuts() before adding new dynamic shortcuts as they are limited
 	 */
-	@Override
 	@WorkerThread
-	public void publishRecentChatsAsShareTargets() {
-		if (!preferenceService.isDirectShare()) {
+	public static void publishRecentChatsAsShareTargets() {
+		if (ThreemaApplication.getServiceManager() == null) {
+			return;
+		}
+
+		if (ThreemaApplication.getServiceManager().getPreferenceService() == null || !ThreemaApplication.getServiceManager().getPreferenceService().isDirectShare()) {
 			return;
 		}
 
-		if (ShortcutManagerCompat.isRateLimitingActive(context)) {
+		if (ShortcutManagerCompat.isRateLimitingActive(getContext())) {
 			logger.info("Shortcuts are currently rate limited. Exiting");
 			return;
 		}
 
-		synchronized (dynamicShortcutLock) {
-			final ConversationService.Filter filter = new ConversationService.Filter() {
-				@Override
-				public boolean onlyUnread() {
+		ConversationService conversationService = null;
+		try {
+			conversationService = ThreemaApplication.getServiceManager().getConversationService();
+		} catch (ThreemaException e) {
+			return;
+		}
+
+		if (conversationService == null) {
+			return;
+		}
+
+		final ConversationService.Filter filter = new ConversationService.Filter() {
+			@Override
+			public boolean onlyUnread() {
 					return false;
 				}
 
-				@Override
-				public boolean noDistributionLists() {
+			@Override
+			public boolean noDistributionLists() {
 					return false;
 				}
 
-				@Override
-				public boolean noHiddenChats() {
+			@Override
+			public boolean noHiddenChats() {
 					return true;
 				}
 
-				@Override
-				public boolean noInvalid() {
+			@Override
+			public boolean noInvalid() {
 					return true;
 				}
-			};
+		};
 
-			final List<ConversationModel> conversations = conversationService.getAll(false, filter);
+		final List<ConversationModel> conversations = conversationService.getAll(false, filter);
 
-			final int numPublishableConversations = Math.min(conversations.size(), Math.min(ShortcutManagerCompat.getMaxShortcutCountPerActivity(context), MAX_SHARE_TARGETS));
+		synchronized (dynamicShortcutLock) {
+			final int numPublishableConversations = Math.min(conversations.size(), Math.min(ShortcutManagerCompat.getMaxShortcutCountPerActivity(getContext()), MAX_SHARE_TARGETS));
 
 			final List<ShortcutInfoCompat> shareTargetShortcuts = new ArrayList<>();
 			for (int i = 0; i < numPublishableConversations; i++) {
@@ -307,8 +332,8 @@ public class ShortcutServiceImpl implements ShortcutService {
 			}
 
 			try {
-				ShortcutManagerCompat.setDynamicShortcuts(context, shareTargetShortcuts);
-				logger.info("Published most recent conversations as sharing target shortcuts");
+				ShortcutManagerCompat.setDynamicShortcuts(getContext(), shareTargetShortcuts);
+				logger.info("Published most recent {} conversations as sharing target shortcuts", numPublishableConversations);
 			} catch (Exception e) {
 				logger.error("Failed setting dynamic shortcuts list ", e);
 			}
@@ -316,14 +341,12 @@ public class ShortcutServiceImpl implements ShortcutService {
 	}
 
 	/**
-	 * Delete all dynamic shortcuts associated with our app. This includes long lived shortcuts,
-	 * This may fail when the user is locked
+	 * Delete all dynamic shortcuts associated with our app.
 	 */
-	@Override
 	@WorkerThread
-	public void deleteAllShareTargetShortcuts() {
+	public static void deleteAllShareTargetShortcuts() {
 		synchronized (dynamicShortcutLock) {
-			List<ShortcutInfoCompat> shortcutInfos = ShortcutManagerCompat.getDynamicShortcuts(context);
+			List<ShortcutInfoCompat> shortcutInfos = ShortcutManagerCompat.getDynamicShortcuts(getContext());
 
 			if (shortcutInfos.size() > 0) {
 				List<String> shortcutIds = new ArrayList<>();
@@ -332,7 +355,7 @@ public class ShortcutServiceImpl implements ShortcutService {
 					shortcutIds.add(shortcutInfoCompat.getId());
 				}
 				try {
-					ShortcutManagerCompat.removeLongLivedShortcuts(context, shortcutIds);
+					ShortcutManagerCompat.removeLongLivedShortcuts(getContext(), shortcutIds);
 				} catch (IllegalStateException e) {
 					logger.error("Failed to remove shortcuts.", e);
 				}
@@ -340,16 +363,25 @@ public class ShortcutServiceImpl implements ShortcutService {
 		}
 	}
 
+	/**
+	 * Delete dynamic shortcut associated with provided message receiver
+	 */
+	@WorkerThread
+	public static void deleteShareTargetShortcut(String uniqueIdString) {
+		synchronized (dynamicShortcutLock) {
+			ShortcutManagerCompat.removeLongLivedShortcuts(getContext(), Collections.singletonList(uniqueIdString));
+		}
+	}
+
 	/**
 	 * Retrieve a bundle with the extras supplied with a shortcut specified by its shortcutId
 	 * @param shortcutId ID of the shortcut to retrieve extras from. The ID equals the MessageReceiver's uniqueId string
 	 * @return A BaseBundle containing the extras identifying the MessageReceiver
 	 */
-	@Override
 	@Nullable
-	public BaseBundle getShareTargetExtrasFromShortcutId(@NonNull String shortcutId) {
+	public static BaseBundle getShareTargetExtrasFromShortcutId(@NonNull String shortcutId) {
 		synchronized (dynamicShortcutLock) {
-			List<ShortcutInfoCompat> shortcutInfos = ShortcutManagerCompat.getDynamicShortcuts(context);
+			List<ShortcutInfoCompat> shortcutInfos = ShortcutManagerCompat.getDynamicShortcuts(getContext());
 
 			if (shortcutInfos.size() > 0) {
 				for (ShortcutInfoCompat shortcutInfoCompat : shortcutInfos) {
@@ -364,7 +396,7 @@ public class ShortcutServiceImpl implements ShortcutService {
 
 	@Nullable
 	@WorkerThread
-	private ShortcutInfoCompat getShareTargetShortcutInfo(@NonNull ConversationModel conversationModel, int rank) {
+	private static ShortcutInfoCompat getShareTargetShortcutInfo(@NonNull ConversationModel conversationModel, int rank) {
 		MessageReceiver messageReceiver = conversationModel.getReceiver();
 
 		if (messageReceiver == null) {
@@ -373,17 +405,27 @@ public class ShortcutServiceImpl implements ShortcutService {
 
 		Person person = null;
 		if (messageReceiver instanceof ContactMessageReceiver) {
-			person = ConversationNotificationUtil.getPerson(contactService, ((ContactMessageReceiver) messageReceiver).getContact(), messageReceiver.getDisplayName());
+			person = ConversationNotificationUtil.getPerson(getContactService(), ((ContactMessageReceiver) messageReceiver).getContact(), messageReceiver.getDisplayName());
+		}
+
+		List<Person> persons = new ArrayList<>();
+		if (messageReceiver instanceof GroupMessageReceiver) {
+			try {
+				Collection<ContactModel> contactModels = ThreemaApplication.getServiceManager().getGroupService().getMembers(conversationModel.getGroup());
+				for(ContactModel contactModel: contactModels) {
+					persons.add(ConversationNotificationUtil.getPerson(getContactService(), contactModel, NameUtil.getDisplayNameOrNickname(contactModel, true)));
+				}
+			} catch (Exception ignore) {}
 		}
 
 		if (messageReceiver.getNotificationAvatar() != null && !TestUtil.empty(messageReceiver.getDisplayName())) {
 			try {
-				ShortcutInfoCompat.Builder shortcutInfoCompatBuilder = new ShortcutInfoCompat.Builder(context, messageReceiver.getUniqueIdString())
+				ShortcutInfoCompat.Builder shortcutInfoCompatBuilder = new ShortcutInfoCompat.Builder(getContext(), messageReceiver.getUniqueIdString())
 					.setIcon(IconCompat.createWithBitmap(messageReceiver.getNotificationAvatar()))
 					.setIntent(getShareTargetShortcutIntent(messageReceiver))
 					.setShortLabel(messageReceiver.getShortName() != null ? messageReceiver.getShortName() : messageReceiver.getDisplayName())
 					.setLongLabel(messageReceiver.getDisplayName())
-					.setActivity(new ComponentName(context, MainActivity.class))
+					.setActivity(new ComponentName(getContext(), MainActivity.class))
 					.setExtras(putShareTargetExtras(messageReceiver))
 					.setLongLived(true)
 					.setRank(rank)
@@ -395,6 +437,10 @@ public class ShortcutServiceImpl implements ShortcutService {
 					shortcutInfoCompatBuilder.setPerson(person);
 				}
 
+				if (persons.size() > 0) {
+					shortcutInfoCompatBuilder.setPersons(persons.toArray(new Person[0]));
+				}
+
 				return shortcutInfoCompatBuilder.build();
 			} catch (Exception e) {
 				logger.debug("Unable to build shortcut", e);
@@ -404,7 +450,7 @@ public class ShortcutServiceImpl implements ShortcutService {
 	}
 
 	@NonNull
-	private PersistableBundle putShareTargetExtras(MessageReceiver<? extends AbstractMessageModel> messageReceiver) {
+	private static PersistableBundle putShareTargetExtras(MessageReceiver<? extends AbstractMessageModel> messageReceiver) {
 		PersistableBundle persistableBundle = new PersistableBundle();
 
 		switch (messageReceiver.getType()) {
@@ -423,4 +469,17 @@ public class ShortcutServiceImpl implements ShortcutService {
 
 		return persistableBundle;
 	}
+
+	private static Context getContext() {
+		return ThreemaApplication.getAppContext();
+	}
+
+	private static ContactService getContactService() {
+		try {
+			return Objects.requireNonNull(ThreemaApplication.getServiceManager()).getContactService();
+		} catch (Exception e) {
+			logger.error("Exception", e);
+		}
+		return null;
+	}
 }

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

@@ -26,6 +26,7 @@ import android.content.Context;
 import java.util.Locale;
 import java.util.concurrent.TimeUnit;
 
+import androidx.annotation.NonNull;
 import ch.threema.app.R;
 
 public class StringConversionUtil {
@@ -40,10 +41,10 @@ public class StringConversionUtil {
 		return new String(bytes);
 	}
 
-	public static String secondsToString(long fullSeconds, boolean longFormat) {
+	public static @NonNull String secondsToString(long fullSeconds, boolean longFormat) {
 		String[] pieces = secondsToPieces(fullSeconds);
 
-		if(longFormat || !pieces[0].equals("00")) {
+		if (longFormat || !pieces[0].equals("00")) {
 			return pieces[0] + ":" + pieces[1] + ":" + pieces[2];
 		}
 		else {
@@ -51,7 +52,7 @@ public class StringConversionUtil {
 		}
 	}
 
-	private static String[] secondsToPieces(long fullSeconds) {
+	private static @NonNull	String[] secondsToPieces(long fullSeconds) {
 		String[] pieces = new String[3];
 
 		pieces[0] = xDigit((int) ((float)fullSeconds / 3600), 2);

+ 2 - 2
app/src/main/java/ch/threema/app/voip/AudioSelectorButton.java

@@ -28,7 +28,6 @@ import android.util.AttributeSet;
 import android.view.View;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.util.Collections;
 import java.util.HashSet;
@@ -43,9 +42,10 @@ import ch.threema.app.voip.VoipAudioManager.AudioDevice;
 import ch.threema.app.voip.listeners.VoipAudioManagerListener;
 import ch.threema.app.voip.managers.VoipListenerManager;
 import ch.threema.app.voip.services.VoipCallService;
+import ch.threema.base.utils.LoggingUtil;
 
 public class AudioSelectorButton extends AppCompatImageView implements View.OnClickListener {
-	private static final Logger logger = LoggerFactory.getLogger(AudioSelectorButton.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("AudioSelectorButton");
 
 	// Constants for Drawable.setAlpha()
 	private static final int HIDDEN = 0;

+ 2 - 2
app/src/main/java/ch/threema/app/voip/CallAnswerIndicatorLayout.java

@@ -27,12 +27,12 @@ import android.widget.ImageView;
 import android.widget.RelativeLayout;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import ch.threema.app.R;
+import ch.threema.base.utils.LoggingUtil;
 
 public class CallAnswerIndicatorLayout extends RelativeLayout {
-	private static final Logger logger = LoggerFactory.getLogger(CallAnswerIndicatorLayout.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("CallAnswerIndicatorLayout");
 
 	// Constants for Drawable.setAlpha()
 	private static final int DARK = 100;

+ 3 - 2
app/src/main/java/ch/threema/app/voip/CallState.java

@@ -22,7 +22,6 @@
 package ch.threema.app.voip;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -33,6 +32,8 @@ import androidx.annotation.AnyThread;
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 
+import ch.threema.base.utils.LoggingUtil;
+
 /**
  * The call state is a combination of the plain state and a call ID.
  *
@@ -44,7 +45,7 @@ import androidx.annotation.NonNull;
  */
 @AnyThread
 public class CallState {
-	private static final Logger logger = LoggerFactory.getLogger(CallState.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("CallState");
 
 	/**
 	 * No call is currently active.

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

@@ -38,7 +38,6 @@ import android.widget.Toast;
 import com.google.protobuf.InvalidProtocolBufferException;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.webrtc.AudioSource;
 import org.webrtc.AudioTrack;
 import org.webrtc.CameraVideoCapturer;
@@ -116,6 +115,7 @@ import ch.threema.app.voip.util.VoipVideoParams;
 import ch.threema.app.webrtc.DataChannelObserver;
 import ch.threema.app.webrtc.UnboundedFlowControlledDataChannel;
 import ch.threema.domain.protocol.api.APIConnector;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.protobuf.callsignaling.CallSignaling;
 import java8.util.concurrent.CompletableFuture;
 import java8.util.stream.StreamSupport;
@@ -128,7 +128,7 @@ import java8.util.stream.StreamSupport;
  */
 public class PeerConnectionClient {
 	// Note: Not static, because we want to set a prefix
-	private final Logger logger = LoggerFactory.getLogger("PeerConnectionClient");
+	private final Logger logger = LoggingUtil.getThreemaLogger("PeerConnectionClient");
 
 	private static final String AUDIO_TRACK_ID = "3MACALLa0";
 	private static final String AUDIO_CODEC_OPUS = "opus";
@@ -402,7 +402,7 @@ public class PeerConnectionClient {
 		VoipUtil.setLoggerPrefix(logger, callId);
 
 		// Create logger for SdpPatcher
-		final Logger sdpPatcherLogger = LoggerFactory.getLogger(PeerConnectionClient.class + ":" + "SdpPatcher");
+		final Logger sdpPatcherLogger = LoggingUtil.getThreemaLogger("PeerConnectionClient:SdpPatcher");
 		VoipUtil.setLoggerPrefix(sdpPatcherLogger, callId);
 
 		// Initialize instance variables
@@ -1663,7 +1663,7 @@ public class PeerConnectionClient {
 	}
 
 	private class DCObserver extends DataChannelObserver {
-		private final @NonNull Logger logger = LoggerFactory.getLogger("SignalingDataChannel");
+		private final @NonNull Logger logger = LoggingUtil.getThreemaLogger("SignalingDataChannel");
 		final @NonNull CompletableFuture<?> openFuture = new CompletableFuture<>();
 
 		@Override

+ 2 - 2
app/src/main/java/ch/threema/app/voip/VoipAudioManager.java

@@ -39,7 +39,6 @@ import android.content.pm.PackageManager;
 import android.media.AudioManager;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.webrtc.ThreadUtils;
 
 import java.util.HashSet;
@@ -53,13 +52,14 @@ import ch.threema.app.notifications.BackgroundErrorNotification;
 import ch.threema.app.voip.listeners.VoipAudioManagerListener;
 import ch.threema.app.voip.managers.VoipListenerManager;
 import ch.threema.app.voip.util.AppRTCUtils;
+import ch.threema.base.utils.LoggingUtil;
 import java8.util.concurrent.CompletableFuture;
 
 /**
  * VoipAudioManager manages all audio related parts of the Threema VoIP calls.
  */
 public class VoipAudioManager {
-	private static final Logger logger = LoggerFactory.getLogger("VoipAudioManager");
+	private static final Logger logger = LoggingUtil.getThreemaLogger("VoipAudioManager");
 	private static final String TAG = "VoipAudioManager";
 
 	/**

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

@@ -47,7 +47,6 @@ import android.os.Looper;
 import android.os.Process;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.webrtc.ThreadUtils;
 
 import java.util.List;
@@ -57,13 +56,14 @@ import ch.threema.app.ThreemaApplication;
 import ch.threema.app.voip.services.VoipCallService;
 import ch.threema.app.voip.util.AppRTCUtils;
 import ch.threema.app.voip.util.VoipUtil;
+import ch.threema.base.utils.LoggingUtil;
 
 /**
  * VoipBluetoothManager manages functions related to Bluetoth devices in
  * Threema voice calls.
  */
 public class VoipBluetoothManager {
-	private static final Logger logger = LoggerFactory.getLogger("VoipBluetoothManager");
+	private static final Logger logger = LoggingUtil.getThreemaLogger("VoipBluetoothManager");
 
 	// Timeout interval for starting or stopping audio to a Bluetooth SCO device.
 	private static final int BLUETOOTH_SCO_TIMEOUT_MS = 4000;

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

@@ -30,7 +30,6 @@ import android.provider.ContactsContract;
 import android.widget.Toast;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import androidx.annotation.Nullable;
 import ch.threema.app.R;
@@ -44,6 +43,7 @@ import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.ContactLookupUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.voip.util.VoipUtil;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.storage.models.ContactModel;
 
 /**
@@ -51,7 +51,7 @@ import ch.threema.storage.models.ContactModel;
  * start the call activity.
  */
 public class CallActionIntentActivity extends ThreemaActivity {
-	private static final Logger logger = LoggerFactory.getLogger(CallActionIntentActivity.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("CallActionIntentActivity");
 	private ServiceManager serviceManager;
 	private ContactService contactService;
 	private PreferenceService preferenceService;

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

@@ -71,7 +71,6 @@ import com.getkeepsafe.taptargetview.TapTarget;
 import com.getkeepsafe.taptargetview.TapTargetView;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.webrtc.RendererCommon;
 import org.webrtc.SurfaceViewRenderer;
 
@@ -112,7 +111,6 @@ import ch.threema.app.listeners.ContactListener;
 import ch.threema.app.listeners.SensorListener;
 import ch.threema.app.managers.ListenerManager;
 import ch.threema.app.managers.ServiceManager;
-import ch.threema.app.push.PushService;
 import ch.threema.app.routines.UpdateFeatureLevelRoutine;
 import ch.threema.app.services.ContactService;
 import ch.threema.app.services.LifetimeService;
@@ -139,7 +137,6 @@ import ch.threema.app.voip.services.VideoContext;
 import ch.threema.app.voip.services.VoipCallService;
 import ch.threema.app.voip.services.VoipStateService;
 import ch.threema.app.voip.util.VoipUtil;
-import ch.threema.app.wearable.WearableHandler;
 import ch.threema.base.utils.Utils;
 import ch.threema.domain.protocol.ThreemaFeature;
 import ch.threema.domain.protocol.api.APIConnector;
@@ -147,6 +144,7 @@ import ch.threema.domain.protocol.csp.messages.voip.VoipCallAnswerData;
 import ch.threema.domain.protocol.csp.messages.voip.VoipCallOfferData;
 import ch.threema.domain.protocol.csp.messages.voip.features.VideoFeature;
 import ch.threema.localcrypto.MasterKey;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.storage.models.ContactModel;
 import java8.util.concurrent.CompletableFuture;
 
@@ -165,7 +163,7 @@ public class CallActivity extends ThreemaActivity implements
 		SensorListener,
 		GenericAlertDialog.DialogClickListener,
 		LifecycleOwner {
-	private static final Logger logger = LoggerFactory.getLogger("CallActivity");
+	private static final Logger logger = LoggingUtil.getThreemaLogger("CallActivity");
 	private static final String LIFETIME_SERVICE_TAG = "CallActivity";
 	private static final String SENSOR_TAG_CALL = "voipcall";
 	public static final String EXTRA_CALL_FROM_SHORTCUT = "shortcut";
@@ -757,6 +755,7 @@ public class CallActivity extends ThreemaActivity implements
 		// Check master key
 		final MasterKey masterKey = ThreemaApplication.getMasterKey();
 		if (masterKey != null && masterKey.isLocked()) {
+			logger.warn("Cannot start call, master key is locked");
 			Toast.makeText(this, R.string.master_key_locked, Toast.LENGTH_LONG).show();
 			finish();
 			return;
@@ -789,22 +788,21 @@ public class CallActivity extends ThreemaActivity implements
 		ListenerManager.contactListeners.add(this.contactListener);
 		VoipListenerManager.audioManagerListener.add(this.audioManagerListener);
 
-		// restore PIP position from preferences
+		// Restore PIP position from preferences
 		pipPosition = preferenceService.getPipPosition();
 		if (pipPosition == 0x00) {
 			pipPosition = PIP_BOTTOM | PIP_LEFT;
 		}
-
 		adjustPipLayout();
 
 		if (!restoreState(getIntent(), savedInstanceState)) {
-			logger.info("Unable to init state. Finishing");
+			logger.warn("Unable to restore state. Finishing");
 			finish();
 			return;
 		}
 
 		// Check for mandatory permissions
-		logger.debug("Checking for audio permission...");
+		logger.info("Checking for audio permission...");
 		this.micPermissionResponse = new CompletableFuture<>();
 		if (ConfigUtils.requestAudioPermissions(this, null, PERMISSION_REQUEST_RECORD_AUDIO)) {
 			this.micPermissionResponse.complete(new PermissionRequestResult(true, true));
@@ -814,8 +812,10 @@ public class CallActivity extends ThreemaActivity implements
 		this.micPermissionResponse
 			.thenAccept((result) -> {
 				if (result.isGranted()) {
+					logger.info("Audio permission granted");
 					initializeActivity(getIntent());
 				} else {
+					logger.warn("Audio permission not granted");
 					Toast.makeText(CallActivity.this, R.string.permission_record_audio_required, Toast.LENGTH_LONG).show();
 					abortWithError(VoipCallAnswerData.RejectReason.DISABLED);
 				}
@@ -1207,7 +1207,7 @@ public class CallActivity extends ThreemaActivity implements
 	@SuppressLint("ClickableViewAccessibility")
 	@UiThread
 	private void initializeActivity(final Intent intent) {
-		logger.debug("initializeActivity");
+		logger.info("Initialize activity");
 
 		final long callId = this.voipStateService.getCallState().getCallId();
 		final Boolean isInitiator = this.voipStateService.isInitiator();
@@ -1436,6 +1436,7 @@ public class CallActivity extends ThreemaActivity implements
 		// Update UI depending on activity mode
 		switch (activityMode) {
 			case MODE_ACTIVE_CALL:
+				logger.info("Activity mode: Active call");
 				this.commonViews.toggleOutgoingVideoButton.setVisibility(ConfigUtils.isVideoCallsEnabled() ? View.VISIBLE : View.GONE);
 				if (this.voipStateService.getCallState().isCalling()) {
 					// Call is already connected
@@ -1454,6 +1455,7 @@ public class CallActivity extends ThreemaActivity implements
 				}
 				break;
 			case MODE_INCOMING_CALL:
+				logger.info("Activity mode: Incoming call");
 				setVolumeControlStream(AudioManager.STREAM_RING);
 				this.commonViews.callStatus.setText(getString(R.string.voip_notification_title));
 				this.commonViews.toggleOutgoingVideoButton.setVisibility(View.GONE);
@@ -1462,6 +1464,7 @@ public class CallActivity extends ThreemaActivity implements
 				}
 				break;
 			case MODE_OUTGOING_CALL:
+				logger.info("Activity mode: Outgoing call");
 				this.commonViews.toggleOutgoingVideoButton.setVisibility(ConfigUtils.isVideoCallsEnabled() ? View.VISIBLE : View.GONE);
 				this.commonViews.callStatus.setText(getString(R.string.voip_status_initializing));
 				// copy over extras from activity
@@ -1504,6 +1507,7 @@ public class CallActivity extends ThreemaActivity implements
 				}
 				break;
 			case MODE_ANSWERED_CALL:
+				logger.info("Activity mode: Answered call");
 				this.commonViews.toggleOutgoingVideoButton.setVisibility(ConfigUtils.isVideoCallsEnabled() ? View.VISIBLE : View.GONE);
 				break;
 			default:
@@ -1731,9 +1735,6 @@ public class CallActivity extends ThreemaActivity implements
 		final Intent answerIntent = new Intent(getIntent());
 		answerIntent.setClass(getApplicationContext(), VoipCallService.class);
 		ContextCompat.startForegroundService(this, answerIntent);
-		if (PushService.playServicesInstalled(getApplicationContext())){
-			WearableHandler.cancelOnWearable(VoipStateService.TYPE_ACTIVITY);
-		}
 	}
 
 	/**

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

@@ -38,7 +38,6 @@ import android.widget.TextView;
 import android.widget.Toast;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.webrtc.IceCandidate;
 import org.webrtc.PeerConnection;
 import org.webrtc.SessionDescription;
@@ -69,6 +68,7 @@ import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.WebRTCUtil;
 import ch.threema.app.voip.PeerConnectionClient;
 import ch.threema.app.voip.util.SdpPatcher;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.logging.WebRTCLoggable;
 import ch.threema.protobuf.callsignaling.CallSignaling;
 import ch.threema.storage.models.ContactModel;
@@ -79,7 +79,7 @@ import static ch.threema.app.preference.SettingsTroubleshootingFragment.THREEMA_
  * An activity to debug problems with WebRTC (in the context of Threema Calls).
  */
 public class WebRTCDebugActivity extends ThreemaToolbarActivity implements PeerConnectionClient.Events, TextEntryDialog.TextEntryDialogClickListener {
-	private static final Logger logger = LoggerFactory.getLogger(WebRTCDebugActivity.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("WebRTCDebugActivity");
 	private static final String DIALOG_TAG_SEND_WEBRTC_DEBUG = "swd";
 
 	// Threema services

+ 2 - 2
app/src/main/java/ch/threema/app/voip/receivers/IncomingMobileCallReceiver.java

@@ -32,7 +32,6 @@ import android.telecom.TelecomManager;
 import android.telephony.TelephonyManager;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.lang.reflect.Method;
 
@@ -41,12 +40,13 @@ import ch.threema.app.ThreemaApplication;
 import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.voip.services.VoipStateService;
 import ch.threema.base.ThreemaException;
+import ch.threema.base.utils.LoggingUtil;
 
 /**
  * Attempt to reject regular phone call if a Threema Call is running
  */
 public class IncomingMobileCallReceiver extends BroadcastReceiver {
-	private static final Logger logger = LoggerFactory.getLogger("IncomingMobileCallReceiver");
+	private static final Logger logger = LoggingUtil.getThreemaLogger("IncomingMobileCallReceiver");
 
 	@Override
 	public void onReceive(Context context, Intent intent) {

+ 2 - 2
app/src/main/java/ch/threema/app/voip/receivers/VoipMediaButtonReceiver.java

@@ -27,7 +27,6 @@ import android.content.Intent;
 import android.view.KeyEvent;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.managers.ServiceManager;
@@ -35,9 +34,10 @@ import ch.threema.app.voip.CallStateSnapshot;
 import ch.threema.app.voip.services.VoipCallService;
 import ch.threema.app.voip.services.VoipStateService;
 import ch.threema.app.voip.util.VoipUtil;
+import ch.threema.base.utils.LoggingUtil;
 
 public class VoipMediaButtonReceiver extends BroadcastReceiver {
-	private static final Logger logger = LoggerFactory.getLogger(VoipMediaButtonReceiver.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("VoipMediaButtonReceiver");
 
 	@Override
 	public void onReceive(Context context, Intent intent) {

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

@@ -25,7 +25,6 @@ import android.content.Context;
 import android.content.Intent;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import androidx.annotation.NonNull;
 import androidx.core.app.FixedJobIntentService;
@@ -36,6 +35,7 @@ import ch.threema.app.voip.activities.CallActivity;
 import ch.threema.app.voip.util.VoipUtil;
 import ch.threema.base.ThreemaException;
 import ch.threema.domain.protocol.csp.messages.voip.VoipCallAnswerData;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.storage.models.ContactModel;
 
 import static ch.threema.app.voip.services.VoipCallService.EXTRA_CALL_ID;
@@ -45,7 +45,7 @@ import static ch.threema.app.voip.services.VoipCallService.EXTRA_CONTACT_IDENTIT
  * A small intent service that rejects an incoming call.
  */
 public class CallRejectService extends FixedJobIntentService {
-	private static final Logger logger = LoggerFactory.getLogger(CallRejectService.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("CallRejectService");
 	public static final String EXTRA_REJECT_REASON = "REJECT_REASON";
 
 	private VoipStateService voipStateService = null;

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

@@ -22,11 +22,9 @@
 package ch.threema.app.voip.services;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.webrtc.CameraVideoCapturer;
 import org.webrtc.EglBase;
 import org.webrtc.JavaI420Buffer;
-import org.webrtc.NV21Buffer;
 import org.webrtc.VideoFrame;
 import org.webrtc.VideoSink;
 
@@ -37,6 +35,7 @@ import java.nio.ByteBuffer;
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import ch.threema.base.utils.LoggingUtil;
 
 /**
  * Encapsulate information required for rendering video.
@@ -44,7 +43,7 @@ import androidx.annotation.Nullable;
  * Instances of this class live in `VoipStateService`.
  */
 public class VideoContext {
-	private static final Logger logger = LoggerFactory.getLogger(VideoContext.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("VideoContext");
 
 	// Camera orientation for VideoContext
 	@Retention(RetentionPolicy.SOURCE)
@@ -236,7 +235,7 @@ public class VideoContext {
 	 * If no target is set using the `setTarget` method, drop frames.
 	 */
 	private static class ProxyVideoSink implements VideoSink {
-		private static final Logger logger = LoggerFactory.getLogger(ProxyVideoSink.class);
+		private static final Logger logger = LoggingUtil.getThreemaLogger("ProxyVideoSink");
 
 		private @Nullable VideoSink target;
 		private final @NonNull String label;

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

@@ -46,7 +46,6 @@ import android.telephony.TelephonyManager;
 import android.widget.Toast;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.webrtc.CameraVideoCapturer;
 import org.webrtc.IceCandidate;
 import org.webrtc.PeerConnection;
@@ -86,7 +85,6 @@ import ch.threema.app.managers.ListenerManager;
 import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.notifications.BackgroundErrorNotification;
 import ch.threema.app.notifications.NotificationBuilderWrapper;
-import ch.threema.app.push.PushService;
 import ch.threema.app.services.ContactService;
 import ch.threema.app.services.NotificationService;
 import ch.threema.app.services.PreferenceService;
@@ -116,7 +114,6 @@ 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.domain.models.VerificationLevel;
 import ch.threema.domain.protocol.ThreemaFeature;
@@ -128,6 +125,7 @@ import ch.threema.domain.protocol.csp.messages.voip.VoipICECandidatesData;
 import ch.threema.domain.protocol.csp.messages.voip.features.FeatureList;
 import ch.threema.domain.protocol.csp.messages.voip.features.VideoFeature;
 import ch.threema.localcrypto.MasterKeyLockedException;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.protobuf.callsignaling.CallSignaling;
 import ch.threema.storage.models.ContactModel;
 import java8.util.function.Supplier;
@@ -143,7 +141,7 @@ import static ch.threema.app.voip.services.VoipStateService.VIDEO_RENDER_FLAG_NO
  * The service keeping track of VoIP call state and the corresponding WebRTC peer connection.
  */
 public class VoipCallService extends LifecycleService implements PeerConnectionClient.Events {
-	private static final Logger logger = LoggerFactory.getLogger("VoipCallService");
+	private static final Logger logger = LoggingUtil.getThreemaLogger("VoipCallService");
 
 	// Intent extras
 	public static final String EXTRA_CALL_ID = "CALL_ID";
@@ -616,12 +614,6 @@ public class VoipCallService extends LifecycleService implements PeerConnectionC
 			return RESTART_BEHAVIOR;
 		}
 
-		// 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 && PushService.playServicesInstalled(getAppContext())) {
-			WearableHandler.cancelOnWearable(VoipStateService.TYPE_ACTIVITY);
-		}
-
 		final VoipICECandidatesData candidatesData =
 			(VoipICECandidatesData) intent.getSerializableExtra(EXTRA_CANDIDATES);
 
@@ -791,9 +783,6 @@ public class VoipCallService extends LifecycleService implements PeerConnectionC
 				}
 			}.execute(new Pair<>(contact, callState.getCallId()));
 		}
-		if (PushService.playServicesInstalled(getAppContext())){
-			WearableHandler.cancelOnWearable(VoipStateService.TYPE_ACTIVITY);
-		}
 		disconnect();
 	}
 
@@ -1486,11 +1475,6 @@ public class VoipCallService extends LifecycleService implements PeerConnectionC
 					}
 				});
 			}
-			WearableHandler.cancelOnWearable(VoipStateService.TYPE_ACTIVITY);
-		}
-
-		if (PushService.playServicesInstalled(getAppContext())){
-			WearableHandler.cancelOnWearable(VoipStateService.TYPE_ACTIVITY);
 		}
 
 		this.preDisconnect(callId);
@@ -1674,9 +1658,6 @@ 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 (PushService.playServicesInstalled(getAppContext())){
-			WearableHandler.cancelOnWearable(VoipStateService.TYPE_ACTIVITY);
-		}
 	}
 
 	//endregion

+ 6 - 32
app/src/main/java/ch/threema/app/voip/services/VoipStateService.java

@@ -44,12 +44,9 @@ import android.text.SpannableString;
 import android.text.style.ForegroundColorSpan;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.webrtc.IceCandidate;
 import org.webrtc.SessionDescription;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.LinkedList;
@@ -58,7 +55,6 @@ import java.util.Map;
 import java.util.Objects;
 
 import androidx.annotation.AnyThread;
-import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
@@ -70,7 +66,6 @@ import ch.threema.app.ThreemaApplication;
 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;
@@ -89,7 +84,6 @@ 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.domain.protocol.csp.connection.MessageQueue;
 import ch.threema.domain.protocol.csp.messages.voip.VoipCallAnswerData;
@@ -103,6 +97,7 @@ import ch.threema.domain.protocol.csp.messages.voip.VoipCallRingingMessage;
 import ch.threema.domain.protocol.csp.messages.voip.VoipICECandidatesData;
 import ch.threema.domain.protocol.csp.messages.voip.VoipICECandidatesMessage;
 import ch.threema.domain.protocol.csp.messages.voip.features.VideoFeature;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.storage.models.ContactModel;
 import java8.util.concurrent.CompletableFuture;
 
@@ -126,20 +121,13 @@ import static ch.threema.app.voip.services.VoipCallService.EXTRA_IS_INITIATOR;
  */
 @AnyThread
 public class VoipStateService implements AudioManager.OnAudioFocusChangeListener {
-	private static final Logger logger = LoggerFactory.getLogger("VoipStateService");
+	private static final Logger logger = LoggingUtil.getThreemaLogger("VoipStateService");
 	private final static String LIFETIME_SERVICE_TAG = "VoipStateService";
 
 	public static final int VIDEO_RENDER_FLAG_NONE = 0x00;
 	public static final int VIDEO_RENDER_FLAG_INCOMING = 0x01;
 	public static final int VIDEO_RENDER_FLAG_OUTGOING = 0x02;
 
-	// component type for wearable
-	@Retention(RetentionPolicy.SOURCE)
-	@IntDef({TYPE_NOTIFICATION, TYPE_ACTIVITY})
-	public @interface Component {}
-	public static final int TYPE_NOTIFICATION = 0;
-	public static final int TYPE_ACTIVITY = 1;
-
 	// system managers
 	private final AudioManager audioManager;
 	private final NotificationManagerCompat notificationManagerCompat;
@@ -191,7 +179,6 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 	private static final int RINGING_TIMEOUT_SECONDS = 60;
 	private static final int VOIP_CONNECTION_LINGER = 1000 * 5;
 
-	private final WearableHandler wearableHandler;
 	private ScreenOffReceiver screenOffReceiver;
 
 	private class ScreenOffReceiver extends BroadcastReceiver {
@@ -219,7 +206,6 @@ 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
@@ -1333,17 +1319,15 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 				this.notificationManagerCompat.cancel(identity, INCOMING_CALL_NOTIFICATION_ID);
 				this.callNotificationTags.remove(identity);
 			} else {
-				logger.warn("No call notification found for {}", identity);
+				logger.warn("No call notification found for {}, number of tags: {}", identity, this.callNotificationTags.size());
+				if (this.callNotificationTags.size() == 0) {
+					this.notificationManagerCompat.cancel(identity, INCOMING_CALL_NOTIFICATION_ID);
+				}
 			}
 			if (this.callNotificationTags.size() == 0) {
 				unregisterScreenOffReceiver();
 			}
 		}
-
-		if (PushService.playServicesInstalled(appContext)){
-			WearableHandler.cancelOnWearable(TYPE_NOTIFICATION);
-			WearableHandler.cancelOnWearable(TYPE_ACTIVITY);
-		}
 	}
 
 	/**
@@ -1357,11 +1341,6 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 			}
 			this.callNotificationTags.clear();
 		}
-
-		if (PushService.playServicesInstalled(appContext)){
-			WearableHandler.cancelOnWearable(TYPE_NOTIFICATION);
-		}
-
 		unregisterScreenOffReceiver();
 	}
 
@@ -1493,11 +1472,6 @@ public class VoipStateService implements AudioManager.OnAudioFocusChangeListener
 			}
 		}
 
-		// WEARABLE
-		if (PushService.playServicesInstalled(appContext)){
-			wearableHandler.showWearableNotification(contact, callId, avatar);
-		}
-
 		// register screen off receiver
 		registerScreenOffReceiver();
 	}

+ 2 - 2
app/src/main/java/ch/threema/app/voip/util/VideoCapturerUtil.java

@@ -24,7 +24,6 @@ package ch.threema.app.voip.util;
 import android.content.Context;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.webrtc.Camera1Enumerator;
 import org.webrtc.Camera2Enumerator;
 import org.webrtc.CameraEnumerator;
@@ -33,12 +32,13 @@ import org.webrtc.CameraVideoCapturer;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.core.util.Pair;
+import ch.threema.base.utils.LoggingUtil;
 
 /**
  * Enumerate and initialize device cameras.
  */
 public class VideoCapturerUtil {
-	private static final Logger logger = LoggerFactory.getLogger(VideoCapturerUtil.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("VideoCapturerUtil");
 
 	/**
 	 * Return a flag indicating whether the Camera2 API should be used or not.

+ 2 - 2
app/src/main/java/ch/threema/app/voip/util/VoipUtil.java

@@ -28,7 +28,6 @@ import android.telephony.TelephonyManager;
 import android.widget.Toast;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.util.Collections;
 
@@ -52,12 +51,13 @@ import ch.threema.app.voip.services.VoipCallService;
 import ch.threema.app.voip.services.VoipStateService;
 import ch.threema.base.ThreemaException;
 import ch.threema.domain.protocol.ThreemaFeature;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.logging.ThreemaLogger;
 import ch.threema.storage.models.ContactModel;
 
 
 public class VoipUtil {
-	private static final Logger logger = LoggerFactory.getLogger(VoipUtil.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("VoipUtil");
 
 	private static final String DIALOG_TAG_FETCHING_FEATURE_MASK = "fetchingFeatureMask";
 

+ 2 - 2
app/src/main/java/ch/threema/app/voip/util/VoipVideoParams.java

@@ -24,12 +24,12 @@ package ch.threema.app.voip.util;
 import com.google.protobuf.ByteString;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import ch.threema.app.utils.RandomUtil;
 import ch.threema.app.voip.signaling.ToSignalingMessage;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.protobuf.callsignaling.CallSignaling;
 import ch.threema.protobuf.callsignaling.CallSignaling.VideoQualityProfile.QualityProfile;
 
@@ -37,7 +37,7 @@ import ch.threema.protobuf.callsignaling.CallSignaling.VideoQualityProfile.Quali
  * Manage video quality profiles.
  */
 public class VoipVideoParams implements ToSignalingMessage {
-	private static final Logger logger = LoggerFactory.getLogger(VoipVideoParams.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("VoipVideoParams");
 
 	private final @Nullable QualityProfile profile;
 	private final int maxBitrateKbps;

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

@@ -59,6 +59,7 @@ import androidx.preference.PreferenceManager;
 import androidx.recyclerview.widget.DefaultItemAnimator;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
+import ch.threema.app.BuildConfig;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.activities.DisableBatteryOptimizationsActivity;
@@ -390,6 +391,15 @@ public class SessionsActivity extends ThreemaToolbarActivity
 		});
 
 		View emptyView = this.findViewById(R.id.empty_frame);
+		TextView emptyTextView = emptyView.findViewById(R.id.empty_text);
+		String emptyText;
+		try {
+			emptyText = serviceManager.getServerAddressProviderService().getServerAddressProvider().getWebServerUrl();
+		} catch (ThreemaException e) {
+			emptyText = BuildConfig.WEB_SERVER_URL;
+		}
+
+		emptyTextView.setText(getString(R.string.webclient_no_sessions_found, emptyText));
 		this.listView.setEmptyView(emptyView);
 		this.reloadSessionList();
 
@@ -962,8 +972,6 @@ public class SessionsActivity extends ThreemaToolbarActivity
 		}
 	}
 
-
-
 	@TargetApi(Build.VERSION_CODES.M)
 	@Override
 	public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {

+ 4 - 5
app/src/main/java/ch/threema/app/webclient/activities/SessionsIntroActivity.java

@@ -23,10 +23,6 @@ package ch.threema.app.webclient.activities;
 
 import android.content.SharedPreferences;
 import android.os.Bundle;
-
-import androidx.annotation.UiThread;
-import androidx.appcompat.app.ActionBar;
-import androidx.preference.PreferenceManager;
 import android.text.Html;
 import android.text.method.LinkMovementMethod;
 import android.view.MenuItem;
@@ -34,6 +30,9 @@ import android.view.View;
 import android.widget.Button;
 import android.widget.TextView;
 
+import androidx.annotation.UiThread;
+import androidx.appcompat.app.ActionBar;
+import androidx.preference.PreferenceManager;
 import ch.threema.app.R;
 import ch.threema.app.activities.ThreemaToolbarActivity;
 
@@ -57,7 +56,7 @@ public class SessionsIntroActivity extends ThreemaToolbarActivity {
 		if (sharedPreferences.getBoolean(getString(R.string.preferences__web_client_welcome_shown), false)) {
 			launchButton.setText(R.string.ok);
 			linkText.setVisibility(View.VISIBLE);
-			linkText.setText(Html.fromHtml("<a href=\"" + getString(R.string.webclient_url)+ "\">" + getString(R.string.new_wizard_more_information) + "</a>"));
+			linkText.setText(Html.fromHtml("<a href=\"" + getString(R.string.webclient_info_url)+ "\">" + getString(R.string.new_wizard_more_information) + "</a>"));
 			linkText.setMovementMethod (LinkMovementMethod.getInstance());
 		} else {
 			linkText.setVisibility(View.GONE);

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

@@ -27,7 +27,6 @@ import android.os.PowerManager;
 import android.text.format.DateUtils;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -37,6 +36,7 @@ import androidx.annotation.NonNull;
 import ch.threema.app.BuildConfig;
 import ch.threema.app.services.LifetimeService;
 import ch.threema.app.utils.ConfigUtils;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.storage.models.WebClientSessionModel;
 
 /**
@@ -45,7 +45,7 @@ import ch.threema.storage.models.WebClientSessionModel;
  */
 @AnyThread
 public class WakeLockServiceImpl implements WakeLockService {
-	private static final Logger logger = LoggerFactory.getLogger("WakeLockService");
+	private static final Logger logger = LoggingUtil.getThreemaLogger("WakeLockService");
 	private static final String WAKELOCK_TAG = BuildConfig.APPLICATION_ID + ":webClientWakeLock";
 	private static final String LIFETIME_SERVICE_TAG = "WakeLockService";
 	private final Context appContext;

+ 3 - 3
app/src/main/java/ch/threema/app/webclient/services/instance/state/SessionConnectionContext.java

@@ -42,7 +42,6 @@ import org.saltyrtc.tasks.webrtc.exceptions.UntiedException;
 import org.saltyrtc.tasks.webrtc.transport.SignalingTransportHandler;
 import org.saltyrtc.tasks.webrtc.transport.SignalingTransportLink;
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.webrtc.DataChannel;
 
 import java.io.IOException;
@@ -77,6 +76,7 @@ import ch.threema.app.webclient.webrtc.TemporaryDataChannelObserver;
 import ch.threema.app.webclient.webrtc.TemporaryTaskEventHandler;
 import ch.threema.app.webrtc.DataChannelObserver;
 import ch.threema.app.webrtc.UnboundedFlowControlledDataChannel;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.logging.ThreemaLogger;
 
 /**
@@ -96,7 +96,7 @@ class SessionConnectionContext {
 	static final int C2C_CONNECT_TIMEOUT_MS = 42000;
 
 	// Logger
-	private final Logger logger = LoggerFactory.getLogger(SessionConnectionContext.class);
+	private final Logger logger = LoggingUtil.getThreemaLogger("SessionConnectionContext");
 
 	// Session context
 	@NonNull final SessionContext ctx;
@@ -365,7 +365,7 @@ class SessionConnectionContext {
 		final UnboundedFlowControlledDataChannel ufcdc = new UnboundedFlowControlledDataChannel(logPrefix, this.sdc);
 
 		// Create signalling data channel logger
-		final Logger sdcLogger = LoggerFactory.getLogger("SignalingDataChannel");
+		final Logger sdcLogger = LoggingUtil.getThreemaLogger("SignalingDataChannel");
 		if (sdcLogger instanceof ThreemaLogger) {
 			((ThreemaLogger) sdcLogger).setPrefix(logPrefix + "." + this.sdc.label() + "/" + this.sdc.id());
 		}

+ 2 - 2
app/src/main/java/ch/threema/app/webrtc/FlowControlledDataChannel.java

@@ -23,11 +23,11 @@ package ch.threema.app.webrtc;
 
 import org.saltyrtc.tasks.webrtc.exceptions.IllegalStateError;
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.webrtc.DataChannel;
 
 import androidx.annotation.AnyThread;
 import androidx.annotation.NonNull;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.logging.ThreemaLogger;
 import java8.util.concurrent.CompletableFuture;
 
@@ -40,7 +40,7 @@ import java8.util.concurrent.CompletableFuture;
  */
 @AnyThread
 public class FlowControlledDataChannel {
-	@NonNull final private Logger logger = LoggerFactory.getLogger("FlowControlledDataChannel");
+	@NonNull final private Logger logger = LoggingUtil.getThreemaLogger("FlowControlledDataChannel");
 	@NonNull public final DataChannel dc;
 	private final long lowWaterMark;
 	private final long highWaterMark;

+ 5 - 6
app/src/main/java/ch/threema/app/webrtc/UnboundedFlowControlledDataChannel.java

@@ -21,17 +21,16 @@
 
 package ch.threema.app.webrtc;
 
-import androidx.annotation.AnyThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
 import org.saltyrtc.tasks.webrtc.exceptions.IllegalStateError;
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.webrtc.DataChannel;
 
 import java.util.concurrent.ExecutionException;
 
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import ch.threema.base.utils.LoggingUtil;
 import java8.util.concurrent.CompletableFuture;
 import java8.util.function.Function;
 
@@ -45,7 +44,7 @@ import java8.util.function.Function;
  */
 @AnyThread
 public class UnboundedFlowControlledDataChannel extends FlowControlledDataChannel {
-	@NonNull final private Logger logger = LoggerFactory.getLogger("UnboundedFlowControlledDataChannel");
+	@NonNull final private Logger logger = LoggingUtil.getThreemaLogger("UnboundedFlowControlledDataChannel");
 	@NonNull private CompletableFuture<Void> queue;
 
 	/**

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

@@ -24,7 +24,6 @@ package ch.threema.app.workers;
 import android.content.Context;
 
 import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.util.HashMap;
 import java.util.List;
@@ -39,12 +38,13 @@ import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.services.ContactService;
 import ch.threema.app.services.PreferenceService;
 import ch.threema.app.utils.ContactUtil;
-import ch.threema.domain.protocol.api.APIConnector;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.domain.models.IdentityState;
+import ch.threema.domain.protocol.api.APIConnector;
 import ch.threema.storage.models.ContactModel;
 
 public class IdentityStatesWorker extends Worker {
-	private static final Logger logger = LoggerFactory.getLogger(IdentityStatesWorker.class);
+	private static final Logger logger = LoggingUtil.getThreemaLogger("IdentityStatesWorker");
 
 	private ContactService contactService;
 	private APIConnector apiConnector;

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

@@ -40,7 +40,7 @@ import ch.threema.logging.backend.LogcatBackend;
  *
  * Do not use this manager directly, instead log through SLF4J! For example:
  *
- *     private static final Logger logger = LoggerFactory.getLogger("ThreemaApplication");
+ *     private static final Logger logger = LoggingUtil.getThreemaLogger("ThreemaApplication");
  *     ...
  *     logger.debug("This is a debug log");
  */
@@ -65,6 +65,9 @@ public class LoggerManager {
 		if (name.startsWith("SaltyRTC.") || name.startsWith("org.saltyrtc")) {
 			return Log.INFO;
 		}
+		if (name.startsWith("libwebrtc") || name.startsWith("org.webrtc")) {
+			return Log.INFO;
+		}
 		return Log.WARN;
 	}
 

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

@@ -46,7 +46,7 @@ import ch.threema.app.services.FileService;
 import ch.threema.app.utils.ZipUtil;
 import ch.threema.app.utils.executor.HandlerExecutor;
 import ch.threema.logging.LogLevel;
-import ch.threema.logging.LoggingUtil;
+import ch.threema.base.utils.LoggingUtil;
 import java8.util.concurrent.CompletableFuture;
 
 /**
@@ -73,6 +73,7 @@ public class DebugLogFileBackend implements LogBackend {
 		"ch.threema.app.",
 		"ch.threema.domain.",
 		"ch.threema.storage.",
+		"ch.threema.",
 	};
 
 	// Worker thread

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

@@ -28,7 +28,7 @@ import org.slf4j.helpers.MessageFormatter;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import ch.threema.logging.LogLevel;
-import ch.threema.logging.LoggingUtil;
+import ch.threema.base.utils.LoggingUtil;
 
 /**
  * A logging backend that logs to the ADB logcat.

+ 0 - 6
app/src/main/java/ch/threema/storage/models/data/media/AudioDataModel.java

@@ -49,12 +49,6 @@ public class AudioDataModel implements MediaMessageDataInterface {
 		this.encryptionKey = encryptedKey;
 		this.isDownloaded = false;
 	}
-	public AudioDataModel(int duration, boolean isDownloaded) {
-		this.duration = duration;
-		this.isDownloaded = isDownloaded;
-		this.audioBlobId = new byte[0];
-		this.encryptionKey = new byte[0];
-	}
 
 	public int getDuration() {
 		return this.duration;

+ 20 - 10
app/src/main/java/ch/threema/storage/models/data/media/FileDataModel.java

@@ -235,32 +235,42 @@ public class FileDataModel implements MediaMessageDataInterface {
 		return null;
 	}
 
-	public String getDurationString() {
+	/**
+	 * Return a formatted string representing the duration as provided by the respective metadata field
+	 * in the format of hours:minutes:seconds
+	 * @return Formatted duration string or 00:00 in case of error
+	 */
+	public @NonNull String getDurationString() {
+		return StringConversionUtil.secondsToString(getDurationSeconds(), false);
+	}
+
+	/**
+	 * Return the duration in SECONDS as set in the metadata field.
+	 */
+	public long getDurationSeconds() {
 		try {
 			Float durationF = getMetaDataFloat(METADATA_KEY_DURATION);
 			if (durationF != null) {
-				long duration = durationF.longValue();
-				if (duration > 0) {
-					return StringConversionUtil.secondsToString(duration, false);
-				}
+				return Math.round(durationF);
 			}
 		} catch (Exception ignored) {}
-		return null;
+		return 0L;
 	}
 
 	/**
-	 * Return the duration in SECONDS as set in the metadata field.
+	 * Return the duration in MILLISECONDS as set in the metadata field.
 	 *
-	 * Note: Floats are converted to long integers.
+	 * Note: Floats are converted to long integers. No rounding.
 	 */
-	public long getDuration() {
+	public long getDurationMs() {
 		try {
 			Float durationF = getMetaDataFloat(METADATA_KEY_DURATION);
 			if (durationF != null) {
+				durationF *= 1000F;
 				return durationF.longValue();
 			}
 		} catch (Exception ignored) {}
-		return 0;
+		return 0L;
 	}
 
 	private void fromString(String s) {

+ 5 - 9
app/src/main/java/ch/threema/storage/models/data/media/VideoDataModel.java

@@ -53,14 +53,10 @@ public class VideoDataModel implements MediaMessageDataInterface {
 		this.videoSize = videoSize;
 	}
 
-	public VideoDataModel(int duration, int videoSize, boolean isDownloaded) {
-		this.duration = duration;
-		this.videoSize = videoSize;
-		this.isDownloaded = isDownloaded;
-		this.videoBlobId = new byte[0];
-		this.encryptionKey = new byte[0];
-	}
-
+	/**
+	 * Get Duration of video in SECONDS
+	 * @return duration
+	 */
 	public int getDuration() {
 		return this.duration;
 	}
@@ -154,7 +150,7 @@ public class VideoDataModel implements MediaMessageDataInterface {
 	 * This method should only be used for backwards compatibility!
 	 */
 	public static VideoDataModel fromFileData(@NonNull FileDataModel fileDataModel) {
-		final int duration = (int) Math.min(fileDataModel.getDuration(), (long) Integer.MAX_VALUE);
+		final int duration = (int) Math.min(fileDataModel.getDurationSeconds(), (long) Integer.MAX_VALUE);
 		final int size = (int) Math.min(fileDataModel.getFileSize(), (long) Integer.MAX_VALUE);
 		return new VideoDataModel(duration, size, fileDataModel.getBlobId(), fileDataModel.getEncryptionKey());
 	}

+ 2 - 2
app/src/main/res/drawable-v24/ic_thumbscroller.xml

@@ -1,7 +1,7 @@
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:aapt="http://schemas.android.com/aapt"
-    android:width="40dp"
-    android:height="64dp"
+    android:width="35dp"
+    android:height="56dp"
     android:viewportWidth="40"
     android:viewportHeight="64">
   <path

+ 2 - 2
app/src/main/res/drawable/ic_thumbscroller.xml

@@ -1,6 +1,6 @@
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="40dp"
-    android:height="64dp"
+    android:width="35dp"
+    android:height="56dp"
     android:viewportWidth="40"
     android:viewportHeight="64">
 <path

Неке датотеке нису приказане због велике количине промена