ソースを参照

Version 6.0.1

Threema 5 ヶ月 前
コミット
f5d44f37b0
100 ファイル変更1171 行追加394 行削除
  1. 4 5
      app/build.gradle.kts
  2. 1 0
      app/src/androidTest/java/ch/threema/app/DangerousTest.java
  3. 1 1
      app/src/androidTest/java/ch/threema/app/TestCoreServiceManager.kt
  4. 1 1
      app/src/androidTest/java/ch/threema/app/backuprestore/csv/BackupServiceTest.java
  5. 1 1
      app/src/androidTest/java/ch/threema/app/processors/MessageProcessorProvider.kt
  6. 102 0
      app/src/androidTest/java/ch/threema/app/utils/FileUtilTest.kt
  7. 1 1
      app/src/androidTest/java/ch/threema/app/webclient/activities/SessionsActivityTest.java
  8. 0 97
      app/src/androidTest/java/ch/threema/logging/backend/DebugLogFileBackendTest.java
  9. 128 0
      app/src/androidTest/java/ch/threema/logging/backend/DebugLogFileBackendTest.kt
  10. 31 21
      app/src/androidTest/java/ch/threema/storage/DatabaseNonceStoreTest.kt
  11. 4 5
      app/src/libre/play/release-notes/de/default.txt
  12. 4 5
      app/src/libre/play/release-notes/en-US/default.txt
  13. 7 0
      app/src/main/java/ch/threema/app/activities/AboutActivity.java
  14. 8 4
      app/src/main/java/ch/threema/app/activities/AddContactActivity.java
  15. 3 0
      app/src/main/java/ch/threema/app/activities/AppLinksActivity.java
  16. 2 0
      app/src/main/java/ch/threema/app/activities/BackupAdminActivity.java
  17. 8 0
      app/src/main/java/ch/threema/app/activities/BackupRestoreProgressActivity.kt
  18. 3 0
      app/src/main/java/ch/threema/app/activities/BiometricLockActivity.java
  19. 8 0
      app/src/main/java/ch/threema/app/activities/BlockedIdentitiesActivity.kt
  20. 3 0
      app/src/main/java/ch/threema/app/activities/ComposeMessageActivity.java
  21. 2 0
      app/src/main/java/ch/threema/app/activities/ContactDetailActivity.java
  22. 7 0
      app/src/main/java/ch/threema/app/activities/CropImageActivity.java
  23. 8 0
      app/src/main/java/ch/threema/app/activities/DirectoryActivity.java
  24. 2 0
      app/src/main/java/ch/threema/app/activities/DisableBatteryOptimizationsActivity.java
  25. 8 0
      app/src/main/java/ch/threema/app/activities/DistributionListAddActivity.kt
  26. 5 0
      app/src/main/java/ch/threema/app/activities/EditSendContactActivity.kt
  27. 3 0
      app/src/main/java/ch/threema/app/activities/EnterSerialActivity.java
  28. 8 0
      app/src/main/java/ch/threema/app/activities/EulaActivity.kt
  29. 8 0
      app/src/main/java/ch/threema/app/activities/ExcludedSyncIdentitiesActivity.kt
  30. 3 0
      app/src/main/java/ch/threema/app/activities/ExportIDActivity.java
  31. 8 0
      app/src/main/java/ch/threema/app/activities/ExportIDResultActivity.java
  32. 3 0
      app/src/main/java/ch/threema/app/activities/GroupAdd2Activity.java
  33. 13 0
      app/src/main/java/ch/threema/app/activities/GroupAddActivity.java
  34. 19 9
      app/src/main/java/ch/threema/app/activities/GroupDetailActivity.java
  35. 33 15
      app/src/main/java/ch/threema/app/activities/HomeActivity.java
  36. 2 0
      app/src/main/java/ch/threema/app/activities/ImagePaintActivity.java
  37. 7 0
      app/src/main/java/ch/threema/app/activities/ImagePaintKeyboardActivity.java
  38. 13 4
      app/src/main/java/ch/threema/app/activities/LicenseActivity.java
  39. 6 0
      app/src/main/java/ch/threema/app/activities/MapActivity.java
  40. 13 0
      app/src/main/java/ch/threema/app/activities/MediaGalleryActivity.java
  41. 3 0
      app/src/main/java/ch/threema/app/activities/MediaViewerActivity.java
  42. 11 1
      app/src/main/java/ch/threema/app/activities/MessageDetailsActivity.kt
  43. 5 0
      app/src/main/java/ch/threema/app/activities/PermissionRequestActivity.kt
  44. 3 0
      app/src/main/java/ch/threema/app/activities/PinLockActivity.java
  45. 15 0
      app/src/main/java/ch/threema/app/activities/PrivacyPolicyActivity.java
  46. 72 43
      app/src/main/java/ch/threema/app/activities/ProblemSolverActivity.kt
  47. 8 0
      app/src/main/java/ch/threema/app/activities/ProfilePicRecipientsActivity.kt
  48. 7 0
      app/src/main/java/ch/threema/app/activities/QRCodeZoomActivity.java
  49. 10 18
      app/src/main/java/ch/threema/app/activities/RecipientListBaseActivity.java
  50. 3 0
      app/src/main/java/ch/threema/app/activities/SMSVerificationLinkActivity.java
  51. 3 0
      app/src/main/java/ch/threema/app/activities/SendMediaActivity.java
  52. 3 0
      app/src/main/java/ch/threema/app/activities/ServerMessageActivity.java
  53. 10 1
      app/src/main/java/ch/threema/app/activities/StarredMessagesActivity.kt
  54. 3 0
      app/src/main/java/ch/threema/app/activities/StickerSelectorActivity.java
  55. 3 0
      app/src/main/java/ch/threema/app/activities/StorageManagementActivity.java
  56. 10 0
      app/src/main/java/ch/threema/app/activities/SupportActivity.java
  57. 8 0
      app/src/main/java/ch/threema/app/activities/TermsOfServiceActivity.kt
  58. 5 0
      app/src/main/java/ch/threema/app/activities/ThreemaPushNotificationInfoActivity.kt
  59. 3 0
      app/src/main/java/ch/threema/app/activities/UnlockMasterKeyActivity.java
  60. 7 6
      app/src/main/java/ch/threema/app/activities/VerificationLevelActivity.java
  61. 8 0
      app/src/main/java/ch/threema/app/activities/WhatsNewActivity.java
  62. 8 0
      app/src/main/java/ch/threema/app/activities/WorkExplainActivity.java
  63. 12 0
      app/src/main/java/ch/threema/app/activities/WorkIntroActivity.kt
  64. 3 0
      app/src/main/java/ch/threema/app/activities/ballot/BallotChooserActivity.java
  65. 3 0
      app/src/main/java/ch/threema/app/activities/ballot/BallotMatrixActivity.java
  66. 3 0
      app/src/main/java/ch/threema/app/activities/ballot/BallotOverviewActivity.java
  67. 3 0
      app/src/main/java/ch/threema/app/activities/ballot/BallotWizardActivity.java
  68. 8 0
      app/src/main/java/ch/threema/app/activities/ballot/BallotWizardFragment0.java
  69. 13 0
      app/src/main/java/ch/threema/app/activities/ballot/BallotWizardFragment1.java
  70. 8 0
      app/src/main/java/ch/threema/app/activities/notificationpolicy/ContactNotificationsActivity.kt
  71. 8 0
      app/src/main/java/ch/threema/app/activities/notificationpolicy/GroupNotificationsActivity.kt
  72. 32 12
      app/src/main/java/ch/threema/app/activities/wizard/WizardBackupRestoreActivity.java
  73. 2 0
      app/src/main/java/ch/threema/app/activities/wizard/WizardBaseActivity.java
  74. 3 0
      app/src/main/java/ch/threema/app/activities/wizard/WizardFingerPrintActivity.java
  75. 3 0
      app/src/main/java/ch/threema/app/activities/wizard/WizardIDRestoreActivity.java
  76. 8 0
      app/src/main/java/ch/threema/app/activities/wizard/WizardIntroActivity.java
  77. 2 0
      app/src/main/java/ch/threema/app/activities/wizard/WizardSafeRestoreActivity.java
  78. 4 1
      app/src/main/java/ch/threema/app/activities/wizard/WizardStartActivity.java
  79. 12 21
      app/src/main/java/ch/threema/app/adapters/ComposeMessageAdapter.java
  80. 0 2
      app/src/main/java/ch/threema/app/adapters/GroupDetailAdapter.java
  81. 1 0
      app/src/main/java/ch/threema/app/adapters/MessageListViewHolder.kt
  82. 55 60
      app/src/main/java/ch/threema/app/adapters/WidgetViewsFactory.java
  83. 2 0
      app/src/main/java/ch/threema/app/archive/ArchiveActivity.java
  84. 73 0
      app/src/main/java/ch/threema/app/backuprestore/ZipFileWrapper.kt
  85. 94 45
      app/src/main/java/ch/threema/app/backuprestore/csv/RestoreService.java
  86. 2 6
      app/src/main/java/ch/threema/app/camera/CameraActivity.java
  87. 6 1
      app/src/main/java/ch/threema/app/camera/CameraFragment.kt
  88. 7 2
      app/src/main/java/ch/threema/app/camera/QRScannerActivity.kt
  89. 3 0
      app/src/main/java/ch/threema/app/dialogs/BallotVoteDialog.java
  90. 13 0
      app/src/main/java/ch/threema/app/dialogs/BottomSheetGridDialog.java
  91. 13 0
      app/src/main/java/ch/threema/app/dialogs/BottomSheetListDialog.java
  92. 8 0
      app/src/main/java/ch/threema/app/dialogs/CallbackTextEntryDialog.kt
  93. 7 0
      app/src/main/java/ch/threema/app/dialogs/CancelableGenericProgressDialog.java
  94. 7 0
      app/src/main/java/ch/threema/app/dialogs/CancelableHorizontalProgressDialog.java
  95. 2 0
      app/src/main/java/ch/threema/app/dialogs/ContactEditDialog.java
  96. 7 6
      app/src/main/java/ch/threema/app/dialogs/ExpandableTextEntryDialog.java
  97. 12 0
      app/src/main/java/ch/threema/app/dialogs/FormatTextEntryDialog.java
  98. 8 0
      app/src/main/java/ch/threema/app/dialogs/GenericAlertDialog.java
  99. 13 0
      app/src/main/java/ch/threema/app/dialogs/GenericProgressDialog.java
  100. 12 0
      app/src/main/java/ch/threema/app/dialogs/GroupDescEditDialog.java

+ 4 - 5
app/build.gradle.kts

@@ -26,7 +26,7 @@ if (gradle.startParameter.taskRequests.toString().contains("Hms")) {
 /**
  * Only use the scheme "<major>.<minor>.<patch>" for the appVersion
  */
-val appVersion = "6.0.0"
+val appVersion = "6.0.1"
 
 /**
  * betaSuffix with leading dash (e.g. `-beta1`).
@@ -35,7 +35,7 @@ val appVersion = "6.0.0"
  */
 val betaSuffix = ""
 
-val defaultVersionCode = 1070
+val defaultVersionCode = 1074
 
 /**
  * Map with keystore paths (if found).
@@ -113,7 +113,7 @@ android {
         stringBuildConfigField("WEB_SERVER_URL", "https://web.threema.ch/")
         stringBuildConfigField("APP_RATING_URL", "https://threema.ch/app-rating/android/{rating}")
         stringBuildConfigField("MAP_STYLES_URL", "https://map.threema.ch/styles/streets/style.json")
-        stringBuildConfigField("MAP_POI_URL", "https://poi.threema.ch/around/{latitude}/{longitude}/{radius}/")
+        stringBuildConfigField("MAP_POI_AROUND_URL", "https://poi.threema.ch/around/{latitude}/{longitude}/{radius}/")
         stringBuildConfigField("MAP_POI_NAMES_URL", "https://poi.threema.ch/names/{latitude}/{longitude}/{query}/")
         byteArrayBuildConfigField("THREEMA_PUSH_PUBLIC_KEY", PublicKeys.threemaPush)
         stringBuildConfigField("ONPREM_ID_PREFIX", "O")
@@ -124,8 +124,6 @@ android {
         booleanBuildConfigField("MD_SYNC_DISTRIBUTION_LISTS", false)
         booleanBuildConfigField("EDIT_MESSAGES_ENABLED", true)
         booleanBuildConfigField("DELETE_MESSAGES_ENABLED", true)
-        booleanBuildConfigField("EMOJI_REACTIONS_ENABLED", true)
-        booleanBuildConfigField("EMOJI_REACTIONS_WEB_ENABLED", true)
 
         // config fields for action URLs / deep links
         stringBuildConfigField("uriScheme", "threema")
@@ -148,6 +146,7 @@ android {
             setOf(
                 "en",
                 "be-rBY",
+                "bg",
                 "ca",
                 "cs",
                 "de",

+ 1 - 0
app/src/androidTest/java/ch/threema/app/DangerousTest.java

@@ -33,4 +33,5 @@ import java.lang.annotation.Target;
 @Target({ElementType.METHOD, ElementType.TYPE})
 @Retention(RetentionPolicy.RUNTIME)
 public @interface DangerousTest {
+    String reason() default "";
 }

+ 1 - 1
app/src/androidTest/java/ch/threema/app/TestCoreServiceManager.kt

@@ -227,7 +227,7 @@ class TestNonceStore : NonceStore {
         scope: NonceScope,
         chunkSize: Int,
         offset: Int,
-        nonces: MutableList<HashedNonce>,
+        hashedNonces: MutableList<HashedNonce>,
     ) {
         // Nothing to do
     }

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

@@ -84,7 +84,7 @@ import static ch.threema.app.PermissionRuleUtilsKt.getReadWriteExternalStoragePe
 
 @RunWith(AndroidJUnit4.class)
 @LargeTest
-@DangerousTest // Deletes data and possibly identity
+@DangerousTest(reason = "Deletes data and possibly identity")
 @Ignore("because this test broke with API version switch introduced in 7ed52bcfedd0bdcd2924ae14afe7ccb7bdc52c7a")
 // TODO(ANDR-1483)
 public class BackupServiceTest {

+ 1 - 1
app/src/androidTest/java/ch/threema/app/processors/MessageProcessorProvider.kt

@@ -567,7 +567,7 @@ open class MessageProcessorProvider {
                 scope: NonceScope,
                 chunkSize: Int,
                 offset: Int,
-                nonces: MutableList<HashedNonce>,
+                hashedNonces: MutableList<HashedNonce>,
             ) {
             }
 

+ 102 - 0
app/src/androidTest/java/ch/threema/app/utils/FileUtilTest.kt

@@ -0,0 +1,102 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2025 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.app.utils
+
+import ch.threema.app.ThreemaApplication
+import kotlin.test.Test
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class FileUtilTest {
+    @Test
+    fun testValidPaths() {
+        // Arrange
+        val paths = listOf(
+            "/data/data/other/app/files/.crs-private_key/",
+            "/sdcard/Download/some_file.txt",
+        )
+
+        paths.forEach { path ->
+            // Act
+            val isSanePath = FileUtil.isSanePath(ThreemaApplication.getAppContext(), path)
+
+            // Assert
+            assertTrue { isSanePath }
+        }
+    }
+
+    @Test
+    fun testValidPathsWithTraversals() {
+        // Arrange
+        val paths = listOf(
+            "/data/data/other/app/../app/./files/.crs-private_key/",
+            "../../../../sdcard/Download/some_file.txt",
+            "/data/../sdcard/Download/some_file.txt",
+        )
+
+        paths.forEach { path ->
+            // Act
+            val isSanePath = FileUtil.isSanePath(ThreemaApplication.getAppContext(), path)
+
+            // Assert
+            assertTrue { isSanePath }
+        }
+    }
+
+    @Test
+    fun testInvalidInternalPaths() {
+        // Arrange
+        val paths = listOf(
+            "/data/data/ch.threema.app/databases/db.db",
+            "/data/data/ch.threema.app/files/file.txt",
+            "/data/data/ch.threema.app/file.txt",
+        )
+
+        paths.forEach { path ->
+            // Act
+            val isSanePath = FileUtil.isSanePath(ThreemaApplication.getAppContext(), path)
+
+            // Assert
+            assertFalse { isSanePath }
+        }
+    }
+
+    @Test
+    fun testInvalidInternalPathsWithPathTraversals() {
+        // Arrange
+        val paths = listOf(
+            "../.././data/data/ch.threema.app/databases/db.db",
+            "/data/data/../data/ch.threema.app/files/file.txt",
+            "/data/data/ch.threema.app/../ch.threema.app/../ch.threema.app/file.txt",
+            "/data/data/.///./ch.threema.app/file.txt",
+            "/data/../../../data/data/ch.threema.app/file.txt",
+        )
+
+        paths.forEach { path ->
+            // Act
+            val isSanePath = FileUtil.isSanePath(ThreemaApplication.getAppContext(), path)
+
+            // Assert
+            assertFalse { isSanePath }
+        }
+    }
+}

+ 1 - 1
app/src/androidTest/java/ch/threema/app/webclient/activities/SessionsActivityTest.java

@@ -69,7 +69,7 @@ import static ch.threema.app.services.BrowserDetectionService.Browser;
  */
 @RunWith(AndroidJUnit4.class)
 @LargeTest
-@DangerousTest // Modifies webclient sessions
+@DangerousTest(reason = "Modifies webclient sessions")
 public class SessionsActivityTest {
     @Rule
     public ActivityTestRule<SessionsActivity> activityTestRule

+ 0 - 97
app/src/androidTest/java/ch/threema/logging/backend/DebugLogFileBackendTest.java

@@ -1,97 +0,0 @@
-/*  _____ _
- * |_   _| |_  _ _ ___ ___ _ __  __ _
- *   | | | ' \| '_/ -_) -_) '  \/ _` |_
- *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
- *
- * Threema for Android
- * Copyright (c) 2018-2025 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.logging.backend;
-
-import android.util.Log;
-
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.concurrent.TimeUnit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.rule.GrantPermissionRule;
-import ch.threema.app.BuildConfig;
-import ch.threema.app.DangerousTest;
-
-import static ch.threema.app.PermissionRuleUtilsKt.getReadWriteExternalStoragePermissionRule;
-
-/**
- * Debug log file test
- */
-@RunWith(AndroidJUnit4.class)
-@DangerousTest // Deletes logfile
-public class DebugLogFileBackendTest {
-
-    @Rule
-    public GrantPermissionRule permissionRule = getReadWriteExternalStoragePermissionRule();
-
-    @Before
-    public void disableLogfile() {
-        DebugLogFileBackend.setEnabled(false);
-    }
-
-    /**
-     * Make sure that logging into the debug log file actually creates the debug log file.
-     * Also test that the file is only created when enabled.
-     */
-    @Test
-    public void testEnable() throws Exception {
-        final File logFilePath = DebugLogFileBackend.getLogFilePath();
-
-        // Log with the debug log file disabled
-        final DebugLogFileBackend backend = new DebugLogFileBackend(Log.INFO);
-        backend.print(Log.WARN, BuildConfig.LOG_TAG, null, "hi");
-
-        // Enabling the debug log file won't create the log file just yet
-        Assert.assertFalse(logFilePath.exists());
-        DebugLogFileBackend.setEnabled(true);
-        Assert.assertFalse(logFilePath.exists());
-
-        // Logs below the min log level are filtered
-        backend.printAsync(Log.DEBUG, BuildConfig.LOG_TAG, null, "hey").get(500, TimeUnit.MILLISECONDS);
-        Assert.assertFalse(logFilePath.exists());
-
-        // Log with the debug log file enabled
-        backend.printAsync(Log.WARN, BuildConfig.LOG_TAG, null, "hi").get(500, TimeUnit.MILLISECONDS);
-        Assert.assertTrue(logFilePath.exists());
-    }
-
-    /**
-     * Make sure that enabling the debug log file actually creates the debug log file.
-     */
-    @Test
-    public void testDisableRemovesFile() throws IOException {
-        final File logFilePath = DebugLogFileBackend.getLogFilePath();
-        Assert.assertFalse(logFilePath.exists());
-        Assert.assertTrue("Could not create logfile", logFilePath.createNewFile());
-        Assert.assertTrue(logFilePath.exists());
-        DebugLogFileBackend.setEnabled(false);
-        Assert.assertFalse(logFilePath.exists());
-    }
-
-}

+ 128 - 0
app/src/androidTest/java/ch/threema/logging/backend/DebugLogFileBackendTest.kt

@@ -0,0 +1,128 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2018-2025 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.logging.backend
+
+import android.util.Log
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.rule.GrantPermissionRule
+import ch.threema.app.BuildConfig
+import ch.threema.app.DangerousTest
+import ch.threema.app.getReadWriteExternalStoragePermissionRule
+import ch.threema.logging.LogLevel
+import java.util.concurrent.TimeUnit
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Rule
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@DangerousTest(reason = "Deletes logfile")
+class DebugLogFileBackendTest {
+
+    @JvmField
+    @Rule
+    val permissionRule: GrantPermissionRule = getReadWriteExternalStoragePermissionRule()
+
+    @BeforeTest
+    fun disableLogfile() {
+        DebugLogFileBackend.setEnabled(false)
+    }
+
+    /**
+     * Make sure that logging into the debug log file actually creates the debug log file.
+     * Also test that the file is only created when enabled.
+     */
+    @Test
+    fun testEnable() {
+        val logFilePath = DebugLogFileBackend.getLogFilePath()
+
+        // Log with the debug log file disabled
+        val backend = DebugLogFileBackend(Log.INFO)
+        backend.printSomething(level = Log.WARN)
+
+        // Enabling the debug log file won't create the log file just yet
+        assertFalse(logFilePath.exists())
+        DebugLogFileBackend.setEnabled(true)
+        assertFalse(logFilePath.exists())
+
+        // Logs below the min log level are filtered
+        backend.printSomething(level = Log.DEBUG)
+        assertFalse(logFilePath.exists())
+
+        // Log with the debug log file enabled
+        backend.printSomething(level = Log.WARN)
+        assertTrue(logFilePath.exists())
+
+        // Verify that the fallback file is not created when not needed
+        assertFalse(DebugLogFileBackend.getFallbackLogFilePath().exists())
+    }
+
+    /**
+     * Make sure that the fallback log file is deleted when the default log file can be created successfully.
+     */
+    @Test
+    fun testFallbackFileIsDeletedIfDefaultFileCanBeCreated() {
+        // Create the fallback log file
+        val fallbackLogFilePath = DebugLogFileBackend.getFallbackLogFilePath()
+        assertTrue(fallbackLogFilePath.createNewFile(), "Could not create fallback logfile")
+
+        // Enable logging and write a log message
+        DebugLogFileBackend.setEnabled(true)
+        val backend = DebugLogFileBackend(Log.INFO)
+        backend.printSomething(level = Log.WARN)
+
+        // Verify that the fallback file is now deleted, as it is not needed
+        assertFalse(fallbackLogFilePath.exists())
+    }
+
+    /**
+     * Make sure that disabling the debug log actually deletes the debug log file.
+     */
+    @Test
+    fun testDisableRemovesFile() {
+        val logFilePath = DebugLogFileBackend.getLogFilePath()
+        assertFalse(logFilePath.exists())
+        assertTrue(logFilePath.createNewFile(), "Could not create logfile")
+        assertTrue(logFilePath.exists())
+        DebugLogFileBackend.setEnabled(false)
+        assertFalse(logFilePath.exists())
+    }
+
+    /**
+     * Make sure that disabling the debug log actually deletes the fallback debug log file.
+     */
+    @Test
+    fun testDisableRemovesFallbackFile() {
+        val fallbackLogFilePath = DebugLogFileBackend.getFallbackLogFilePath()
+        assertFalse(fallbackLogFilePath.exists())
+        assertTrue(fallbackLogFilePath.createNewFile(), "Could not create fallback logfile")
+        assertTrue(fallbackLogFilePath.exists())
+        DebugLogFileBackend.setEnabled(false)
+        assertFalse(fallbackLogFilePath.exists())
+    }
+
+    private fun DebugLogFileBackend.printSomething(@LogLevel level: Int) {
+        printAsync(level, BuildConfig.LOG_TAG, null, "hi").get(500, TimeUnit.MILLISECONDS)
+    }
+}

+ 31 - 21
app/src/androidTest/java/ch/threema/storage/DatabaseNonceStoreTest.kt

@@ -38,9 +38,9 @@ import org.junit.Test
 class DatabaseNonceStoreTest {
     private lateinit var tempDbFileName: String
 
-    private var _store: DatabaseNonceStore? = null
+    private lateinit var _store: DatabaseNonceStore
     private val store: NonceStore
-        get() = _store!!
+        get() = _store
 
     @Before
     fun setup() {
@@ -55,8 +55,7 @@ class DatabaseNonceStoreTest {
 
     @After
     fun teardown() {
-        _store!!.close()
-        _store = null
+        _store.close()
         ApplicationProvider
             .getApplicationContext<ThreemaApplication>()
             .deleteDatabase(tempDbFileName)
@@ -152,17 +151,18 @@ class DatabaseNonceStoreTest {
     }
 
     @Test
-    fun testBulkImportNotHashed() {
+    fun testBulkImportHashed() {
         assertStoreEmpty()
 
         val nonces = createNonces()
-        val pseudoHashedNonces = nonces.map { HashedNonce(it.bytes) }
+        val hashedNonces = nonces.map { hashNonce(it) }
 
-        // Insert the unhashed nonces. As they are inserted as if they were already hashed,
+        // Insert the hashed nonces. As they are inserted as if they were already hashed,
         // the store must not hash them again.
-        assertTrue(store.insertHashedNonces(NonceScope.CSP, pseudoHashedNonces))
-        assertTrue(store.insertHashedNonces(NonceScope.D2D, pseudoHashedNonces))
+        assertTrue(store.insertHashedNonces(NonceScope.CSP, hashedNonces))
+        assertTrue(store.insertHashedNonces(NonceScope.D2D, hashedNonces))
 
+        // Assert that all unhashed nonces are detected as existing in the store
         nonces.forEach {
             // Assert that the nonce as inserted should exist
             assertTrue(store.exists(NonceScope.CSP, it))
@@ -171,22 +171,32 @@ class DatabaseNonceStoreTest {
     }
 
     @Test
-    fun testBulkImportHashed() {
+    fun testGettingRawNoncesAsHashedNonces() {
         assertStoreEmpty()
-
         val nonces = createNonces()
-        val hashedNonces = nonces.map { hashNonce(it) }
 
-        // Insert the hashed nonces. As they are inserted as if they were already hashed,
-        // the store must not hash them again.
-        assertTrue(store.insertHashedNonces(NonceScope.CSP, hashedNonces))
-        assertTrue(store.insertHashedNonces(NonceScope.D2D, hashedNonces))
+        // Insert raw nonces without hashing them. This test is important as this may still be the case on some devices as nonces used to be stored
+        // without being hashed first.
+        nonces.forEach { nonce ->
+            val statement = _store.writableDatabase.compileStatement("INSERT INTO nonce_csp VALUES (?)")
+            statement.bindBlob(1, nonce.bytes)
+            statement.executeInsert()
+        }
 
-        // Assert that all unhashed nonces exist in the store
-        nonces.forEach {
-            // Assert that the nonce as inserted should exist
-            assertTrue(store.exists(NonceScope.CSP, it))
-            assertTrue(store.exists(NonceScope.D2D, it))
+        // Check that insertion worked
+        nonces.forEach { nonce ->
+            assertTrue(store.exists(NonceScope.CSP, nonce))
+        }
+        assertEquals(256, store.getCount(NonceScope.CSP))
+
+        // Act
+        val hashedNonces = store.getAllHashedNonces(NonceScope.CSP)
+
+        // Assert
+        assertEquals(256, hashedNonces.size)
+        assertEquals(256, store.getCount(NonceScope.CSP))
+        nonces.forEach { nonce ->
+            assertTrue(store.exists(NonceScope.CSP, nonce))
         }
     }
 

+ 4 - 5
app/src/libre/play/release-notes/de/default.txt

@@ -1,5 +1,4 @@
-- Die Android-App lässt sich nun mit der Beta-Version von Threema 2.0 für Desktop nutzen («Hauptmenü > Threema 2.0 für Desktop (Beta)»)
-- Behebung verschiedener Fehler bei der Suche innerhalb eines Chats
-- Korrekte Anzeige des Dateinamens bei PDFs
-- Verbesserungen in Zusammenhang mit der Benachrichtigung bei Einzelanrufen
-- Verbesserung des Speicherverbrauchs bei der Wiederherstellung von grossen Daten-Backups
+- Bulgarisch als Sprache hinzugefügt
+- Behebung eines Fehlers, wodurch Threema 2.0 für Desktop auf gewissen Geräten nicht genutzt werden konnte
+- Behebung eines Fehlers beim Threema-Widget
+- Behebung einer Sicherheitslücke, die ältere Android-Versionen betraf (bis Android 10): Eine bösartige App, die auf demselben Gerät wie Threema installiert war, hätte Nutzer dazu verleiten können, eine beliebige Datei an einen bestehenden Kontakt zu senden (gemeldet von Retr02332)

+ 4 - 5
app/src/libre/play/release-notes/en-US/default.txt

@@ -1,5 +1,4 @@
-- The Android app can now be used with the beta version of Threema 2.0 for desktop (“Main menu > Threema 2.0 for desktop (beta)”)
-- Fixed various bugs that could occur when searching within a chat
-- Display the correct name of a PDF file
-- Improved notification of 1:1 calls
-- Improved memory consumption when restoring large data backups
+- Added Bulgarian localization
+- Fixed a bug that caused Threema 2.0 for desktop not to work on certain devices
+- Fixed a bug in relation to the Threema widget
+- Fixed a vulnerability affecting older Android versions (up to Android 10): a malicious app installed on the same device as Threema could have tricked the user into sending any file to an existing contact (reported by Retr02332)

+ 7 - 0
app/src/main/java/ch/threema/app/activities/AboutActivity.java

@@ -26,16 +26,23 @@ import android.view.MenuItem;
 import android.widget.ImageView;
 import android.widget.Toast;
 
+import org.slf4j.Logger;
+
 import androidx.appcompat.app.ActionBar;
 import ch.threema.app.R;
 import ch.threema.app.utils.AnimationUtil;
 import ch.threema.app.utils.ConfigUtils;
+import ch.threema.base.utils.LoggingUtil;
+
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class AboutActivity extends ThreemaToolbarActivity {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("AboutActivity");
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         ActionBar actionBar = getSupportActionBar();
         if (actionBar != null) {

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

@@ -68,8 +68,8 @@ import ch.threema.app.utils.IntentDataUtil;
 import ch.threema.app.utils.QRScannerUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.executor.BackgroundExecutor;
-import ch.threema.app.webclient.services.QRCodeParser;
-import ch.threema.app.webclient.services.QRCodeParserImpl;
+import ch.threema.app.webclient.services.WebSessionQRCodeParser;
+import ch.threema.app.webclient.services.WebSessionQRCodeParserImpl;
 import ch.threema.base.utils.Base64;
 import ch.threema.base.utils.LoggingUtil;
 import ch.threema.data.repositories.ContactModelRepository;
@@ -77,6 +77,7 @@ import ch.threema.domain.protocol.api.APIConnector;
 import ch.threema.storage.models.ContactModel;
 
 import static ch.threema.app.services.QRCodeServiceImpl.QR_TYPE_ID;
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 import static ch.threema.domain.protocol.csp.ProtocolDefines.IDENTITY_LEN;
 
 public class AddContactActivity extends ThreemaActivity implements GenericAlertDialog.DialogClickListener, NewContactDialog.NewContactDialogClickListener {
@@ -98,6 +99,7 @@ public class AddContactActivity extends ThreemaActivity implements GenericAlertD
     private final BackgroundExecutor backgroundExecutor = new BackgroundExecutor();
 
     public void onCreate(Bundle savedInstanceState) {
+        logScreenVisibility(this, logger);
         ServiceManager serviceManager = ThreemaApplication.getServiceManager();
 
         if (serviceManager == null) {
@@ -224,6 +226,7 @@ public class AddContactActivity extends ThreemaActivity implements GenericAlertD
 
     @SuppressLint("StaticFieldLeak")
     private void addContactByQRResult(final QRCodeService.QRCodeContentResult qrResult) {
+        logger.info("Adding contact from QR code");
         if (qrResult.getExpirationDate() != null
             && qrResult.getExpirationDate().before(new Date())) {
             GenericAlertDialog.newInstance(R.string.title_adduser, getString(R.string.expired_barcode), R.string.ok, 0).show(getSupportFragmentManager(), "ex");
@@ -370,13 +373,13 @@ public class AddContactActivity extends ThreemaActivity implements GenericAlertD
                 try {
                     byte[] base64Payload = Base64.decode(payload);
                     if (base64Payload != null) {
-                        final QRCodeParser webClientQRCodeParser = new QRCodeParserImpl();
+                        final WebSessionQRCodeParser webClientQRCodeParser = new WebSessionQRCodeParserImpl();
                         webClientQRCodeParser.parse(base64Payload); // throws if QR is not valid
                         // it was a valid web client qr code, exit method
                         startWebClientByQRResult(base64Payload);
                         return;
                     }
-                } catch (IOException | QRCodeParser.InvalidQrCodeException x) {
+                } catch (IOException | WebSessionQRCodeParser.InvalidQrCodeException x) {
                     // not a valid base64 or web client payload
                     // ignore and continue
                 }
@@ -436,6 +439,7 @@ public class AddContactActivity extends ThreemaActivity implements GenericAlertD
 
     @Override
     public void onScanButtonClick(String tag) {
+        logger.info("Scan button clicked");
         scanQR();
     }
 }

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

@@ -49,6 +49,8 @@ import ch.threema.domain.protocol.api.APIConnector;
 import ch.threema.domain.protocol.csp.ProtocolDefines;
 import ch.threema.storage.models.ContactModel;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class AppLinksActivity extends ThreemaToolbarActivity {
     private final static Logger logger = LoggingUtil.getThreemaLogger("AppLinksActivity");
 
@@ -58,6 +60,7 @@ public class AppLinksActivity extends ThreemaToolbarActivity {
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         checkLock();
     }

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

@@ -22,6 +22,7 @@
 package ch.threema.app.activities;
 
 import static ch.threema.app.services.PreferenceService.LockingMech_NONE;
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 import android.content.Intent;
 import android.os.Bundle;
@@ -61,6 +62,7 @@ public class BackupAdminActivity extends ThreemaToolbarActivity {
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         isUnlocked = false;
         safeConfig = ThreemaSafeMDMConfig.getInstance();

+ 8 - 0
app/src/main/java/ch/threema/app/activities/BackupRestoreProgressActivity.kt

@@ -37,12 +37,20 @@ import ch.threema.app.ThreemaApplication
 import ch.threema.app.backuprestore.csv.BackupService
 import ch.threema.app.backuprestore.csv.RestoreService
 import ch.threema.app.utils.ConfigUtils
+import ch.threema.app.utils.logScreenVisibility
+import ch.threema.base.utils.LoggingUtil
+
+private val logger = LoggingUtil.getThreemaLogger("BackupRestoreProgressActivity")
 
 /**
  * This activity is shown when the user opens Threema while a backup is being created or restored.
  * This is useful to get a hint about the progress if notifications are not allowed or activated.
  */
 class BackupRestoreProgressActivity : AppCompatActivity() {
+    init {
+        logScreenVisibility(logger)
+    }
+
     private lateinit var titleTextView: TextView
     private lateinit var infoTextView: TextView
     private lateinit var durationDelimiter: View

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

@@ -48,6 +48,8 @@ import ch.threema.app.utils.NavigationUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.base.utils.LoggingUtil;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class BiometricLockActivity extends ThreemaAppCompatActivity {
     private static final Logger logger = LoggingUtil.getThreemaLogger("BiometricLockActivity");
 
@@ -64,6 +66,7 @@ public class BiometricLockActivity extends ThreemaAppCompatActivity {
         logger.debug("onCreate");
 
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         ServiceManager serviceManager = ThreemaApplication.getServiceManager();
         if (serviceManager == null) {

+ 8 - 0
app/src/main/java/ch/threema/app/activities/BlockedIdentitiesActivity.kt

@@ -23,8 +23,16 @@ package ch.threema.app.activities
 
 import ch.threema.app.R
 import ch.threema.app.ThreemaApplication
+import ch.threema.app.utils.logScreenVisibility
+import ch.threema.base.utils.LoggingUtil
+
+private val logger = LoggingUtil.getThreemaLogger("BlockedIdentitiesActivity")
 
 class BlockedIdentitiesActivity : IdentityListActivity() {
+    init {
+        logScreenVisibility(logger)
+    }
+
     private val identityList: IdentityList? by lazy {
         val blockedIdentitiesService =
             ThreemaApplication.getServiceManager()?.blockedIdentitiesService ?: return@lazy null

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

@@ -49,6 +49,8 @@ import ch.threema.app.utils.IntentDataUtil;
 import ch.threema.base.utils.LoggingUtil;
 import ch.threema.localcrypto.MasterKey;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class ComposeMessageActivity extends ThreemaToolbarActivity implements GenericAlertDialog.DialogClickListener {
     private static final Logger logger = LoggingUtil.getThreemaLogger("ComposeMessageActivity");
 
@@ -72,6 +74,7 @@ public class ComposeMessageActivity extends ThreemaToolbarActivity implements Ge
         getWindow().setAllowEnterTransitionOverlap(true);
         getWindow().setAllowReturnTransitionOverlap(true);
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         // Tell the Window that our app is going to responsible for fitting for any system windows.
         // This is similar to the now deprecated:

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

@@ -121,6 +121,7 @@ import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.GroupModel;
 
 import static ch.threema.app.utils.QRScannerUtil.REQUEST_CODE_QR_SCANNER;
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class ContactDetailActivity extends ThreemaToolbarActivity
     implements LifecycleOwner,
@@ -308,6 +309,7 @@ public class ContactDetailActivity extends ThreemaToolbarActivity
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         this.identity = this.getIntent().getStringExtra(ThreemaApplication.INTENT_DATA_CONTACT);
         if (this.identity == null || this.identity.length() == 0) {

+ 7 - 0
app/src/main/java/ch/threema/app/activities/CropImageActivity.java

@@ -42,14 +42,20 @@ import com.google.android.material.appbar.MaterialToolbar;
 import com.google.android.material.color.DynamicColors;
 import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
 
+import org.slf4j.Logger;
+
 import java.util.Collections;
 
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.utils.BitmapUtil;
 import ch.threema.app.utils.ConfigUtils;
+import ch.threema.base.utils.LoggingUtil;
+
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class CropImageActivity extends ThreemaToolbarActivity {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("CropImageActivity");
 
     public static final String EXTRA_ASPECT_X = "ax";
     public static final String EXTRA_ASPECT_Y = "ay";
@@ -69,6 +75,7 @@ public class CropImageActivity extends ThreemaToolbarActivity {
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
+        logScreenVisibility(this, logger);
         Intent intent = getIntent();
         Bundle extras = intent.getExtras();
 

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

@@ -82,6 +82,7 @@ import ch.threema.domain.protocol.api.work.WorkDirectoryContact;
 import ch.threema.domain.protocol.api.work.WorkOrganization;
 
 import static ch.threema.app.ui.DirectoryDataSource.MIN_SEARCH_STRING_LENGTH;
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class DirectoryActivity extends ThreemaToolbarActivity implements ThreemaSearchView.OnQueryTextListener, MultiChoiceSelectorDialog.SelectorDialogClickListener {
     private static final Logger logger = LoggingUtil.getThreemaLogger("DirectoryActivity");
@@ -139,6 +140,12 @@ public class DirectoryActivity extends ThreemaToolbarActivity implements Threema
         }
     };
 
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
+    }
+
     @Override
     public boolean onQueryTextSubmit(String query) {
         // Do nothing
@@ -240,6 +247,7 @@ public class DirectoryActivity extends ThreemaToolbarActivity implements Threema
         directoryAdapter.setOnClickItemListener(new DirectoryAdapter.OnClickItemListener() {
             @Override
             public void onClick(WorkDirectoryContact workDirectoryContact, int position) {
+                logger.info("Directory contact clicked");
                 launchContact(workDirectoryContact, position);
             }
 

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

@@ -47,6 +47,7 @@ import ch.threema.base.utils.LoggingUtil;
 
 import static ch.threema.app.fragments.BackupDataFragment.REQUEST_ID_DISABLE_BATTERY_OPTIMIZATIONS;
 import static ch.threema.app.utils.PowermanagerUtil.isIgnoringBatteryOptimizations;
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 /**
  * Guides user through the process of disabling battery optimization energy saving option.
@@ -97,6 +98,7 @@ public class DisableBatteryOptimizationsActivity extends ThreemaActivity impleme
     @TargetApi(Build.VERSION_CODES.M)
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         if (isIgnoringBatteryOptimizations(this)) {
             setResult(RESULT_OK);

+ 8 - 0
app/src/main/java/ch/threema/app/activities/DistributionListAddActivity.kt

@@ -33,10 +33,18 @@ import ch.threema.app.services.DistributionListService
 import ch.threema.app.ui.SingleToast
 import ch.threema.app.utils.LogUtil
 import ch.threema.app.utils.RuntimeUtil
+import ch.threema.app.utils.logScreenVisibility
+import ch.threema.base.utils.LoggingUtil
 import ch.threema.storage.models.ContactModel
 import ch.threema.storage.models.DistributionListModel
 
+private val logger = LoggingUtil.getThreemaLogger("DistributionListAddActivity")
+
 class DistributionListAddActivity : MemberChooseActivity(), TextEntryDialogClickListener {
+    init {
+        logScreenVisibility(logger)
+    }
+
     private lateinit var distributionListService: DistributionListService
     private var distributionListModel: DistributionListModel? = null
     private var selectedContacts: List<ContactModel> = emptyList()

+ 5 - 0
app/src/main/java/ch/threema/app/activities/EditSendContactActivity.kt

@@ -48,6 +48,7 @@ import ch.threema.app.R
 import ch.threema.app.mediaattacher.ContactEditViewModel
 import ch.threema.app.ui.VCardPropertyView
 import ch.threema.app.utils.VCardExtractor
+import ch.threema.app.utils.logScreenVisibility
 import ch.threema.base.utils.LoggingUtil
 import com.google.android.material.appbar.AppBarLayout
 import com.google.android.material.appbar.MaterialToolbar
@@ -65,6 +66,10 @@ private val logger = LoggingUtil.getThreemaLogger("EditSendContactActivity")
  * it in a chat. The name of the contact can be modified.
  */
 class EditSendContactActivity : ThreemaToolbarActivity() {
+    init {
+        logScreenVisibility(logger)
+    }
+
     private lateinit var viewModel: ContactEditViewModel
     private lateinit var toolbar: MaterialToolbar
     private lateinit var appBarLayout: AppBarLayout

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

@@ -67,6 +67,8 @@ import ch.threema.app.utils.executor.BackgroundExecutor;
 import ch.threema.app.utils.executor.BackgroundTask;
 import ch.threema.base.utils.LoggingUtil;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 // this should NOT extend ThreemaToolbarActivity
 public class EnterSerialActivity extends ThreemaActivity {
     private static final Logger logger = LoggingUtil.getThreemaLogger("EnterSerialActivity");
@@ -89,6 +91,7 @@ public class EnterSerialActivity extends ThreemaActivity {
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         if (!ConfigUtils.isSerialLicensed()) {
             finish();

+ 8 - 0
app/src/main/java/ch/threema/app/activities/EulaActivity.kt

@@ -23,8 +23,16 @@ package ch.threema.app.activities
 
 import ch.threema.app.R
 import ch.threema.app.utils.ConfigUtils
+import ch.threema.app.utils.logScreenVisibility
+import ch.threema.base.utils.LoggingUtil
+
+private val logger = LoggingUtil.getThreemaLogger("EulaActivity")
 
 class EulaActivity : SimpleWebViewActivity() {
+    init {
+        logScreenVisibility(logger)
+    }
+
     override fun getWebViewTitle(): Int {
         return R.string.eula
     }

+ 8 - 0
app/src/main/java/ch/threema/app/activities/ExcludedSyncIdentitiesActivity.kt

@@ -23,8 +23,16 @@ package ch.threema.app.activities
 
 import ch.threema.app.R
 import ch.threema.app.ThreemaApplication
+import ch.threema.app.utils.logScreenVisibility
+import ch.threema.base.utils.LoggingUtil
+
+private val logger = LoggingUtil.getThreemaLogger("ExcludedSyncIdentitiesActivity")
 
 class ExcludedSyncIdentitiesActivity : IdentityListActivity() {
+    init {
+        logScreenVisibility(logger)
+    }
+
     private val identityList: IdentityList? by lazy {
         val listService = ThreemaApplication.getServiceManager()?.excludedSyncIdentitiesService
             ?: return@lazy null

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

@@ -43,6 +43,8 @@ import ch.threema.base.ThreemaException;
 import ch.threema.base.utils.LoggingUtil;
 import ch.threema.domain.identitybackup.IdentityBackupGenerator;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class ExportIDActivity extends AppCompatActivity implements PasswordEntryDialog.PasswordEntryDialogClickListener {
     private static final Logger logger = LoggingUtil.getThreemaLogger("ExportIDActivity");
 
@@ -54,6 +56,7 @@ public class ExportIDActivity extends AppCompatActivity implements PasswordEntry
 
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         final ServiceManager serviceManager = ThreemaApplication.getServiceManager();
         preferenceService = serviceManager.getPreferenceService();

+ 8 - 0
app/src/main/java/ch/threema/app/activities/ExportIDResultActivity.java

@@ -47,6 +47,8 @@ import androidx.lifecycle.LifecycleOwner;
 
 import com.google.android.material.appbar.MaterialToolbar;
 
+import org.slf4j.Logger;
+
 import java.io.ByteArrayOutputStream;
 
 import ch.threema.app.R;
@@ -57,8 +59,13 @@ import ch.threema.app.ui.QRCodePopup;
 import ch.threema.app.ui.TooltipPopup;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.TestUtil;
+import ch.threema.base.utils.LoggingUtil;
+
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class ExportIDResultActivity extends ThreemaToolbarActivity implements GenericAlertDialog.DialogClickListener, LifecycleOwner {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("ExportIDResultActivity");
+
     private static final String DIALOG_TAG_QUIT_CONFIRM = "qconf";
     private static final int QRCODE_SMALL_DIMENSION_PIXEL = 200;
 
@@ -71,6 +78,7 @@ public class ExportIDResultActivity extends ThreemaToolbarActivity implements Ge
 
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         this.toolbar = findViewById(R.id.toolbar);
         setSupportActionBar(this.toolbar);

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

@@ -46,6 +46,8 @@ import ch.threema.base.utils.LoggingUtil;
 import ch.threema.data.models.GroupModel;
 import kotlinx.coroutines.Deferred;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class GroupAdd2Activity extends GroupEditActivity implements ContactEditDialog.ContactEditDialogClickListener {
     private static final Logger logger = LoggingUtil.getThreemaLogger("GroupAdd2Activity");
 
@@ -62,6 +64,7 @@ public class GroupAdd2Activity extends GroupEditActivity implements ContactEditD
     public void onCreate(Bundle savedInstanceState) {
         logger.debug("onCreate");
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         final ActionBar actionBar = getSupportActionBar();
         if (actionBar != null) {

+ 13 - 0
app/src/main/java/ch/threema/app/activities/GroupAddActivity.java

@@ -25,6 +25,8 @@ import android.content.Intent;
 import android.os.Bundle;
 import android.widget.Toast;
 
+import org.slf4j.Logger;
+
 import androidx.annotation.NonNull;
 
 import java.util.ArrayList;
@@ -42,10 +44,15 @@ import ch.threema.app.services.GroupService;
 import ch.threema.app.restrictions.AppRestrictionUtil;
 import ch.threema.app.utils.IntentDataUtil;
 import ch.threema.app.utils.LogUtil;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.GroupModel;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class GroupAddActivity extends MemberChooseActivity implements GenericAlertDialog.DialogClickListener {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("GroupAddActivity");
+
     private static final String BUNDLE_EXISTING_MEMBERS = "ExMem";
     private static final String DIALOG_TAG_NO_MEMBERS = "NoMem";
     private static final String DIALOG_TAG_NOTE_GROUP_HOWTO = "note_group_hint";
@@ -54,6 +61,12 @@ public class GroupAddActivity extends MemberChooseActivity implements GenericAle
     private GroupModel groupModel;
     private boolean appendMembers;
 
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
+    }
+
     @Override
     protected boolean initActivity(Bundle savedInstanceState) {
         if (!super.initActivity(savedInstanceState)) {

+ 19 - 9
app/src/main/java/ch/threema/app/activities/GroupDetailActivity.java

@@ -128,6 +128,7 @@ import kotlinx.coroutines.Deferred;
 
 import static ch.threema.app.adapters.GroupDetailAdapter.GroupDescState.COLLAPSED;
 import static ch.threema.app.adapters.GroupDetailAdapter.GroupDescState.NONE;
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class GroupDetailActivity extends GroupEditActivity implements SelectorDialog.SelectorDialogClickListener,
     GenericAlertDialog.DialogClickListener,
@@ -310,6 +311,7 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         this.myIdentity = userService.getIdentity();
 
@@ -407,6 +409,7 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
             actionBar.setDisplayHomeAsUpEnabled(true);
 
             floatingActionButton.setOnClickListener(v -> {
+                logger.info("FAB (save group settings) clicked");
                 saveGroupSettings();
             });
             groupNameEditText.setMaxByteSize(GroupModel.GROUP_NAME_MAX_LENGTH_BYTES);
@@ -1106,16 +1109,20 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
         if (selectorInfo.contactModel != null) {
             switch (selectorInfo.optionsMap.get(which)) {
                 case SELECTOR_OPTION_CONTACT_DETAIL:
+                    logger.info("Contact details button clicked");
                     launchContactDetail(selectorInfo.view, selectorInfo.contactModel.getIdentity());
                     break;
                 case SELECTOR_OPTION_CHAT:
+                    logger.info("Chat button clicked");
                     showConversation(selectorInfo.contactModel.getIdentity());
                     finish();
                     break;
                 case SELECTOR_OPTION_REMOVE:
+                    logger.info("Kick user button clicked");
                     removeMemberFromGroup(selectorInfo.contactModel);
                     break;
                 case SELECTOR_OPTION_CALL:
+                    logger.info("Call button clicked");
                     VoipUtil.initiateCall(this, selectorInfo.contactModel, false, null);
                     break;
                 default:
@@ -1133,6 +1140,7 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
         // text entry dialog
         switch (tag) {
             case DIALOG_TAG_CLONE_GROUP:
+                logger.info("Clone group dialog confirmed");
                 cloneGroup(text);
                 break;
             default:
@@ -1171,18 +1179,23 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
     public void onYes(String tag, Object data) {
         switch (tag) {
             case DIALOG_TAG_LEAVE_GROUP:
+                logger.info("Leave group dialog confirmed");
                 leaveGroupAndQuit();
                 break;
             case DIALOG_TAG_DISSOLVE_GROUP:
+                logger.info("Dissolve group dialog confirmed");
                 dissolveGroupAndQuit();
                 break;
             case DIALOG_TAG_DELETE_GROUP:
+                logger.info("Delete group dialog confirmed");
                 deleteGroupAndQuit();
                 break;
             case DIALOG_TAG_QUIT:
+                logger.info("Save group changes dialog confirmed");
                 saveGroupSettings();
                 break;
             case DIALOG_TAG_CLONE_GROUP_CONFIRM:
+                logger.info("Clone group info dialog confirmed");
                 showCloneDialog();
                 break;
             default:
@@ -1198,6 +1211,7 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
     @Override
     public void handleOnBackPressed() {
         if (this.operationMode == MODE_EDIT && hasChanges()) {
+            logger.info("Showing warning about unsaved changes");
             GenericAlertDialog.newInstance(
                     R.string.leave,
                     R.string.save_group_changes,
@@ -1308,17 +1322,9 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
         }
     }
 
-    @Override
-    public void onGroupOwnerClick(View v, String identity) {
-        Intent intent = new Intent(this, ContactDetailActivity.class);
-        intent.putExtra(ThreemaApplication.INTENT_DATA_CONTACT, groupModel.getCreatorIdentity());
-        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-        ActivityOptionsCompat options = ActivityOptionsCompat.makeScaleUpAnimation(v, 0, 0, v.getWidth(), v.getHeight());
-        ActivityCompat.startActivityForResult((Activity) this, intent, ThreemaActivity.ACTIVITY_ID_CONTACT_DETAIL, options.toBundle());
-    }
-
     @Override
     public void onGroupMemberClick(View v, @NonNull ContactModel contactModel) {
+        logger.info("Group member clicked");
         String identity = contactModel.getIdentity();
         String shortName = NameUtil.getShortName(contactModel);
 
@@ -1361,22 +1367,26 @@ public class GroupDetailActivity extends GroupEditActivity implements SelectorDi
 
     @Override
     public void onResetLinkClick() {
+        logger.info("Reset link button clicked");
         RuntimeUtil.runOnUiThread(() -> ShowOnceDialog.newInstance(R.string.reset_default_group_link_title, R.string.reset_default_group_link_desc).show(getSupportFragmentManager(), DIALOG_SHOW_ONCE_RESET_LINK_INFO));
     }
 
     @Override
     public void onShareLinkClick() {
+        logger.info("Share link button clicked");
         // option only enabled if there is a default link
         groupInviteService.shareGroupLink(this, groupInviteService.getDefaultGroupInvite(groupModel).get());
     }
 
     @Override
     public void onGroupDescriptionEditClick() {
+        logger.info("Edit description button clicked");
         showGroupDescEditDialog();
     }
 
     @Override
     public void onAddMembersClick(View v) {
+        logger.info("Add member button clicked");
         addNewMembers();
     }
 

+ 33 - 15
app/src/main/java/ch/threema/app/activities/HomeActivity.java

@@ -181,6 +181,8 @@ import ch.threema.storage.models.ConversationModel;
 import ch.threema.storage.models.ConversationTag;
 import ch.threema.storage.models.MessageState;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class HomeActivity extends ThreemaAppCompatActivity implements
     SMSVerificationDialog.SMSVerificationDialogCallback,
     GenericAlertDialog.DialogClickListener,
@@ -324,20 +326,6 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
                     return true;
                 }
 
-                @Override
-                public boolean noDistributionLists() {
-                    return false;
-                }
-
-                @Override
-                public boolean noHiddenChats() {
-                    return false;
-                }
-
-                @Override
-                public boolean noInvalid() {
-                    return false;
-                }
             });
 
             int unread = 0;
@@ -627,6 +615,7 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
         ConfigUtils.configureSystemBars(this);
 
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         if (BackupService.isRunning() || RestoreService.isRunning()) {
             startActivity(new Intent(this, BackupRestoreProgressActivity.class));
@@ -751,6 +740,7 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
     @Override
     protected void onStart() {
         super.onStart();
+        logger.info("HomeActivity started");
 
         if (serviceManager != null) {
 
@@ -793,7 +783,8 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
                 ConfigUtils.isBackgroundDataRestricted(ThreemaApplication.getAppContext(), false) ||
                 ConfigUtils.isNotificationsDisabled(ThreemaApplication.getAppContext()) ||
                 (preferenceService.isVoipEnabled() && ConfigUtils.isFullScreenNotificationsDisabled(ThreemaApplication.getAppContext())) ||
-                ((preferenceService.useThreemaPush() || BuildFlavor.getCurrent().getForceThreemaPush()) && !PowermanagerUtil.isIgnoringBatteryOptimizations(ThreemaApplication.getAppContext()));
+                ((preferenceService.useThreemaPush() || BuildFlavor.getCurrent().getForceThreemaPush()) && !PowermanagerUtil.isIgnoringBatteryOptimizations(ThreemaApplication.getAppContext())) ||
+                (Build.VERSION.SDK_INT < Build.VERSION_CODES.N && preferenceService.getLastDeprecatedAndroidVersionWarningDismissed() == null);
     }
 
     private void showWhatsNew() {
@@ -1311,19 +1302,25 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
             Fragment currentFragment = getSupportFragmentManager().findFragmentByTag(currentFragmentTag);
             if (currentFragment != null) {
                 if (item.getItemId() == R.id.contacts) {
+                    logger.info("Contacts tab clicked");
                     if (!FRAGMENT_TAG_CONTACTS.equals(currentFragmentTag)) {
+                        logger.info("Switching to Contacts tab");
                         getSupportFragmentManager().beginTransaction().setCustomAnimations(R.anim.fast_fade_in, R.anim.fast_fade_out, R.anim.fast_fade_in, R.anim.fast_fade_out).hide(currentFragment).show(contactsFragment).commit();
                         currentFragmentTag = FRAGMENT_TAG_CONTACTS;
                     }
                     return true;
                 } else if (item.getItemId() == R.id.messages) {
+                    logger.info("Messages tab clicked");
                     if (!FRAGMENT_TAG_MESSAGES.equals(currentFragmentTag)) {
+                        logger.info("Switching to Messages tab");
                         getSupportFragmentManager().beginTransaction().setCustomAnimations(R.anim.fast_fade_in, R.anim.fast_fade_out, R.anim.fast_fade_in, R.anim.fast_fade_out).hide(currentFragment).show(messagesFragment).commit();
                         currentFragmentTag = FRAGMENT_TAG_MESSAGES;
                     }
                     return true;
                 } else if (item.getItemId() == R.id.my_profile) {
+                    logger.info("Profile tab clicked");
                     if (!FRAGMENT_TAG_PROFILE.equals(currentFragmentTag)) {
+                        logger.info("Switching to My Profile tab");
                         getSupportFragmentManager().beginTransaction().setCustomAnimations(R.anim.fast_fade_in, R.anim.fast_fade_out, R.anim.fast_fade_in, R.anim.fast_fade_out).hide(currentFragment).show(profileFragment).commit();
                         currentFragmentTag = FRAGMENT_TAG_PROFILE;
                     }
@@ -1505,38 +1502,54 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
         Intent intent = null;
         final int id = item.getItemId();
         if (id == android.R.id.home) {
+            logger.info("Own avatar clicked");
             showQRPopup();
             return true;
         } else if (id == R.id.menu_lock) {
+            logger.info("Lock button clicked");
             lockAppService.lock();
             return true;
         } else if (id == R.id.menu_new_group) {
+            logger.info("New group button clicked");
             intent = new Intent(this, GroupAddActivity.class);
         } else if (id == R.id.menu_new_distribution_list) {
+            logger.info("New distribution list button clicked");
             intent = new Intent(this, DistributionListAddActivity.class);
         } else if (id == R.id.group_requests) {
+            logger.info("Group requests button clicked");
             intent = new Intent(this, OutgoingGroupRequestActivity.class);
         } else if (id == R.id.my_backups) {
+            logger.info("Backups button clicked");
             intent = new Intent(this, BackupAdminActivity.class);
         } else if (id == R.id.webclient) {
+            logger.info("Web button clicked");
             intent = new Intent(this, SessionsActivity.class);
         } else if (id == R.id.multi_device) {
+            logger.info("MD button clicked");
             intent = new Intent(this, LinkedDevicesActivity.class);
         } else if (id == R.id.scanner) {
+            logger.info("QR scanner button clicked");
             intent = new Intent(this, BaseQrScannerActivity.class);
         } else if (id == R.id.help) {
+            logger.info("Help button clicked");
             intent = new Intent(this, SupportActivity.class);
         } else if (id == R.id.settings) {
+            logger.info("Settings button clicked");
             startActivityForResult(new Intent(this, SettingsActivity.class), ThreemaActivity.ACTIVITY_ID_SETTINGS);
         } else if (id == R.id.directory) {
+            logger.info("Directory button clicked");
             intent = new Intent(this, DirectoryActivity.class);
         } else if (id == R.id.threema_channel) {
+            logger.info("Threema channel button clicked");
             confirmThreemaChannel();
         } else if (id == R.id.archived) {
+            logger.info("Archive button clicked");
             intent = new Intent(this, ArchiveActivity.class);
         } else if (id == R.id.globalsearch) {
+            logger.info("Global search button clicked");
             intent = new Intent(this, GlobalSearchActivity.class);
         } else if (id == R.id.starred_messages) {
+            logger.info("Starred messages button clicked");
             intent = new Intent(this, StarredMessagesActivity.class);
         }
 
@@ -1561,6 +1574,7 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
             PorterDuff.Mode.SRC_IN);
         toolbarLogoMain.setContentDescription(getString(R.string.logo));
         toolbarLogoMain.setOnClickListener(v -> {
+            logger.info("Logo clicked");
             if (currentFragmentTag != null) {
                 Fragment currentFragment = getSupportFragmentManager().findFragmentByTag(currentFragmentTag);
                 if (currentFragment != null && currentFragment.isAdded() && !currentFragment.isHidden()) {
@@ -1759,15 +1773,18 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
     public void onYes(String tag, Object data) {
         switch (tag) {
             case DIALOG_TAG_VERIFY_CODE_CONFIRM:
+                logger.info("Verify code confirmed");
                 reallyRequestCall();
                 break;
             case DIALOG_TAG_CANCEL_VERIFY:
                 reallyCancelVerify();
                 break;
             case DIALOG_TAG_MASTERKEY_LOCKED:
+                logger.info("Retrying unlocking master key confirmed");
                 startActivityForResult(new Intent(this, UnlockMasterKeyActivity.class), ThreemaActivity.ACTIVITY_ID_UNLOCK_MASTER_KEY);
                 break;
             case DIALOG_TAG_SERIAL_LOCKED:
+                logger.info("Retrying entering valid license confirmed");
                 startActivityForResult(new Intent(this, EnterSerialActivity.class), ThreemaActivity.ACTIVITY_ID_ENTER_SERIAL);
                 finish();
                 break;
@@ -1775,6 +1792,7 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
                 System.exit(0);
                 break;
             case DIALOG_TAG_THREEMA_CHANNEL_VERIFY:
+                logger.info("Add Threema channel confirmed");
                 addThreemaChannel();
                 break;
             case DIALOG_TAG_PASSWORD_PRESET_CONFIRM:

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

@@ -24,6 +24,7 @@ package ch.threema.app.activities;
 import static ch.threema.app.utils.BitmapUtil.FLIP_HORIZONTAL;
 import static ch.threema.app.utils.BitmapUtil.FLIP_NONE;
 import static ch.threema.app.utils.BitmapUtil.FLIP_VERTICAL;
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 import android.annotation.SuppressLint;
 import android.app.Activity;
@@ -457,6 +458,7 @@ public class ImagePaintActivity extends ThreemaToolbarActivity implements Generi
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
 

+ 7 - 0
app/src/main/java/ch/threema/app/activities/ImagePaintKeyboardActivity.java

@@ -36,6 +36,8 @@ import android.view.ViewTreeObserver;
 import android.view.inputmethod.EditorInfo;
 import android.widget.TextView;
 
+import org.slf4j.Logger;
+
 import androidx.annotation.ColorInt;
 import androidx.appcompat.app.ActionBar;
 import androidx.appcompat.content.res.AppCompatResources;
@@ -47,8 +49,12 @@ import ch.threema.app.R;
 import ch.threema.app.motionviews.widget.TextEntity;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.EditTextUtil;
+import ch.threema.base.utils.LoggingUtil;
+
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class ImagePaintKeyboardActivity extends ThreemaToolbarActivity {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("ImagePaintKeyboardActivity");
 
     public final static String INTENT_EXTRA_TEXT = "text";
     public final static String INTENT_EXTRA_COLOR = "color"; // resolved color
@@ -59,6 +65,7 @@ public class ImagePaintKeyboardActivity extends ThreemaToolbarActivity {
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         this.currentKeyboardHeight = 0;
 

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

@@ -23,14 +23,23 @@ package ch.threema.app.activities;
 
 import android.os.Bundle;
 
-import androidx.appcompat.app.ActionBar;
-
-import android.view.MenuItem;
-import android.webkit.WebView;
+import org.slf4j.Logger;
 
 import ch.threema.app.R;
+import ch.threema.base.utils.LoggingUtil;
+
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class LicenseActivity extends SimpleWebViewActivity {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("LicenseActivity");
+
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
+    }
+
     @Override
     protected int getWebViewTitle() {
         return R.string.os_licenses;

+ 6 - 0
app/src/main/java/ch/threema/app/activities/MapActivity.java

@@ -91,6 +91,7 @@ import static ch.threema.app.utils.IntentDataUtil.INTENT_DATA_LOCATION_LAT;
 import static ch.threema.app.utils.IntentDataUtil.INTENT_DATA_LOCATION_LNG;
 import static ch.threema.app.utils.IntentDataUtil.INTENT_DATA_LOCATION_NAME;
 import static ch.threema.app.utils.IntentDataUtil.INTENT_DATA_LOCATION_PROVIDER;
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class MapActivity extends ThreemaActivity implements GenericAlertDialog.DialogClickListener {
     private static final Logger logger = LoggingUtil.getThreemaLogger("MapActivity");
@@ -122,6 +123,7 @@ public class MapActivity extends ThreemaActivity implements GenericAlertDialog.D
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         ConfigUtils.configureSystemBars(this);
 
@@ -190,6 +192,10 @@ public class MapActivity extends ThreemaActivity implements GenericAlertDialog.D
         initUi();
         try {
             var mapStyleUrl = serverAddressProvider.getMapStyleUrl();
+            if (mapStyleUrl == null) {
+                finish();
+                return;
+            }
             initMap(mapStyleUrl);
         } catch (ThreemaException e) {
             logger.error("Failed to get map style url", e);

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

@@ -22,6 +22,7 @@
 package ch.threema.app.activities;
 
 import static ch.threema.app.utils.RecyclerViewUtil.thumbScrollerPopupStyle;
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 import android.Manifest;
 import android.animation.LayoutTransition;
@@ -174,18 +175,23 @@ public class MediaGalleryActivity extends ThreemaToolbarActivity implements
         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
             int itemId = item.getItemId();
             if (itemId == R.id.menu_message_discard) {
+                logger.info("Discard messages clicked");
                 discardMessages();
                 return true;
             } else if (itemId == R.id.menu_message_save) {
+                logger.info("Save messages clicked");
                 saveMessages();
                 return true;
             } else if (itemId == R.id.menu_share) {
+                logger.info("Share messages clicked");
                 shareMessages();
                 return true;
             } else if (itemId == R.id.menu_show_in_chat) {
+                logger.info("Show in chat clicked");
                 showInChat();
                 return true;
             } else if (itemId == R.id.menu_select_all) {
+                logger.info("Select all clicked");
                 selectAllMessages();
                 return true;
             }
@@ -257,6 +263,7 @@ public class MediaGalleryActivity extends ThreemaToolbarActivity implements
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
     }
 
     @Override
@@ -731,12 +738,14 @@ public class MediaGalleryActivity extends ThreemaToolbarActivity implements
     @Override
     public void onClick(@Nullable AbstractMessageModel messageModel, @Nullable View view, int position) {
         if (actionMode != null) {
+            logger.info("Message selection toggled");
             mediaGalleryAdapter.toggleChecked(position);
             if (mediaGalleryAdapter.getCheckedItemsCount() > 0) {
                 if (actionMode != null) {
                     actionMode.invalidate();
                 }
             } else {
+                logger.info("Deselected last message");
                 actionMode.finish();
             }
         } else {
@@ -754,8 +763,10 @@ public class MediaGalleryActivity extends ThreemaToolbarActivity implements
                             break;
                         case MessageContentsType.FILE:
                             if ((FileUtil.isImageFile(messageModel.getFileData()) || FileUtil.isVideoFile(messageModel.getFileData()) || FileUtil.isAudioFile(messageModel.getFileData()))) {
+                                logger.info("Media file clicked, showing");
                                 showInMediaFragment(messageModel, view);
                             } else {
+                                logger.info("File clicked, opening");
                                 decryptAndShow(messageModel, view, progressBar);
                             }
                             break;
@@ -770,10 +781,12 @@ public class MediaGalleryActivity extends ThreemaToolbarActivity implements
     @Override
     public boolean onLongClick(@Nullable AbstractMessageModel messageModel, @Nullable View itemView, int position) {
         if (actionMode != null) {
+            logger.info("Long pressed message, leaving selection mode");
             actionMode.finish();
         }
         mediaGalleryAdapter.toggleChecked(position);
         if (mediaGalleryAdapter.getCheckedItemsCount() > 0) {
+            logger.info("Long pressed message, entering selection mode");
             actionMode = startSupportActionMode(new MediaGalleryAction());
         }
         return true;

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

@@ -97,6 +97,8 @@ import ch.threema.storage.models.GroupMessageModel;
 import ch.threema.storage.models.MessageType;
 import ch.threema.storage.models.data.MessageContentsType;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class MediaViewerActivity extends ThreemaToolbarActivity implements
     ExpandableTextEntryDialog.ExpandableTextEntryDialogClickListener {
 
@@ -136,6 +138,7 @@ public class MediaViewerActivity extends ThreemaToolbarActivity implements
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
     }
 
     @Override

+ 11 - 1
app/src/main/java/ch/threema/app/activities/MessageDetailsActivity.kt

@@ -58,6 +58,7 @@ import ch.threema.app.ui.CustomTextSelectionCallback
 import ch.threema.app.utils.ConfigUtils
 import ch.threema.app.utils.IntentDataUtil
 import ch.threema.app.utils.LinkifyUtil
+import ch.threema.app.utils.logScreenVisibility
 import ch.threema.base.utils.LoggingUtil
 import ch.threema.storage.models.AbstractMessageModel
 import ch.threema.storage.models.MessageType
@@ -65,6 +66,10 @@ import com.google.android.material.appbar.MaterialToolbar
 import org.slf4j.Logger
 
 class MessageDetailsActivity : ThreemaToolbarActivity(), DialogClickListener {
+    init {
+        logScreenVisibility(logger)
+    }
+
     private companion object {
         val logger: Logger = LoggingUtil.getThreemaLogger("MessageDetailsActivity")
 
@@ -123,7 +128,10 @@ class MessageDetailsActivity : ThreemaToolbarActivity(), DialogClickListener {
 
             override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
                 when (item.itemId) {
-                    CONTEXT_MENU_FORWARD -> forwardText()
+                    CONTEXT_MENU_FORWARD -> {
+                        logger.info("Forward message clicked")
+                        forwardText()
+                    }
                     else -> return false
                 }
                 return true
@@ -252,6 +260,7 @@ class MessageDetailsActivity : ThreemaToolbarActivity(), DialogClickListener {
         toolbar.setNavigationOnClickListener { finish() }
         toolbar.setOnMenuItemClickListener { item: MenuItem ->
             if (item.itemId == R.id.enable_formatting) {
+                logger.info("Toggle formatting clicked")
                 onToggleFormattingClicked(item)
                 return@setOnMenuItemClickListener true
             }
@@ -274,6 +283,7 @@ class MessageDetailsActivity : ThreemaToolbarActivity(), DialogClickListener {
 
     override fun onYes(tag: String, data: Any) {
         if (LinkifyUtil.DIALOG_TAG_CONFIRM_LINK == tag) {
+            logger.info("Opening of link confirmed")
             LinkifyUtil.getInstance().openLink(data as Uri, null, this)
         }
     }

+ 5 - 0
app/src/main/java/ch/threema/app/activities/PermissionRequestActivity.kt

@@ -48,6 +48,7 @@ import ch.threema.app.ui.PermissionIconView
 import ch.threema.app.ui.PermissionIconView.PermissionIconState
 import ch.threema.app.utils.ConfigUtils
 import ch.threema.app.utils.PermissionRequest
+import ch.threema.app.utils.logScreenVisibility
 import ch.threema.base.utils.LoggingUtil
 
 private val logger = LoggingUtil.getThreemaLogger("PermissionRequestActivity")
@@ -61,6 +62,10 @@ private val logger = LoggingUtil.getThreemaLogger("PermissionRequestActivity")
  * [INTENT_PERMISSION_REQUESTS].
  */
 class PermissionRequestActivity : ThreemaActivity() {
+    init {
+        logScreenVisibility(logger)
+    }
+
     companion object {
         const val INTENT_PERMISSION_REQUESTS = "permission_requests_extra"
     }

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

@@ -47,6 +47,8 @@ import ch.threema.app.utils.NavigationUtil;
 import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.base.utils.LoggingUtil;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class PinLockActivity extends ThreemaActivity {
     private static final Logger logger = LoggingUtil.getThreemaLogger("PinLockActivity");
     private static final long ERROR_MESSAGE_TIMEOUT = 3000;
@@ -68,6 +70,7 @@ public class PinLockActivity extends ThreemaActivity {
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         logger.debug("onCreate");
 

+ 15 - 0
app/src/main/java/ch/threema/app/activities/PrivacyPolicyActivity.java

@@ -21,10 +21,25 @@
 
 package ch.threema.app.activities;
 
+import android.os.Bundle;
+
+import org.slf4j.Logger;
+
 import ch.threema.app.R;
 import ch.threema.app.utils.ConfigUtils;
+import ch.threema.base.utils.LoggingUtil;
+
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class PrivacyPolicyActivity extends SimpleWebViewActivity {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("PrivacyPolicyActivity");
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
+    }
+
     @Override
     protected int getWebViewTitle() {
         return R.string.privacy_policy;

+ 72 - 43
app/src/main/java/ch/threema/app/activities/ProblemSolverActivity.kt

@@ -33,14 +33,24 @@ import android.widget.LinearLayout
 import android.widget.TextView
 import androidx.activity.result.ActivityResult
 import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.view.isVisible
 import ch.threema.app.R
 import ch.threema.app.ThreemaApplication
 import ch.threema.app.utils.ConfigUtils
 import ch.threema.app.utils.PowermanagerUtil
+import ch.threema.app.utils.logScreenVisibility
+import ch.threema.base.utils.LoggingUtil
 import com.google.android.material.appbar.MaterialToolbar
 import com.google.android.material.button.MaterialButton
+import java.time.Instant
+
+private val logger = LoggingUtil.getThreemaLogger("ProblemSolverActivity")
 
 class ProblemSolverActivity : ThreemaToolbarActivity() {
+    init {
+        logScreenVisibility(logger)
+    }
+
     private val settingsLauncher = registerForActivityResult(
         ActivityResultContracts.StartActivityForResult(),
     ) { _: ActivityResult? -> recreate() }
@@ -55,7 +65,7 @@ class ProblemSolverActivity : ThreemaToolbarActivity() {
         // explanation text used in problem solver box
         val explanation: Int,
         // the action to call for fixing the problem
-        val intentAction: String,
+        val intentAction: String?,
         // the check to determine if the problem exists
         val check: Boolean,
     )
@@ -63,36 +73,42 @@ class ProblemSolverActivity : ThreemaToolbarActivity() {
     @SuppressLint("InlinedApi")
     private val problems = arrayOf(
         Problem(
-            R.string.problemsolver_title_background,
-            R.string.problemsolver_explain_background,
-            Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
-            ConfigUtils.isBackgroundRestricted(ThreemaApplication.getAppContext()),
+            title = R.string.problemsolver_title_background,
+            explanation = R.string.problemsolver_explain_background,
+            intentAction = Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
+            check = ConfigUtils.isBackgroundRestricted(ThreemaApplication.getAppContext()),
+        ),
+        Problem(
+            title = R.string.problemsolver_title_background_data,
+            explanation = R.string.problemsolver_explain_background_data,
+            intentAction = Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS,
+            check = ConfigUtils.isBackgroundDataRestricted(ThreemaApplication.getAppContext(), false),
         ),
         Problem(
-            R.string.problemsolver_title_background_data,
-            R.string.problemsolver_explain_background_data,
-            Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS,
-            ConfigUtils.isBackgroundDataRestricted(ThreemaApplication.getAppContext(), false),
+            title = R.string.problemsolver_title_notifications,
+            explanation = R.string.problemsolver_explain_notifications,
+            intentAction = Settings.ACTION_APP_NOTIFICATION_SETTINGS,
+            check = ConfigUtils.isNotificationsDisabled(ThreemaApplication.getAppContext()),
         ),
         Problem(
-            R.string.problemsolver_title_notifications,
-            R.string.problemsolver_explain_notifications,
-            Settings.ACTION_APP_NOTIFICATION_SETTINGS,
-            ConfigUtils.isNotificationsDisabled(ThreemaApplication.getAppContext()),
+            title = R.string.problemsolver_title_fullscreen_notifications,
+            explanation = R.string.problemsolver_explain_fullscreen_notifications,
+            intentAction = Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT,
+            check = ConfigUtils.isFullScreenNotificationsDisabled(ThreemaApplication.getAppContext()),
         ),
         Problem(
-            R.string.problemsolver_title_fullscreen_notifications,
-            R.string.problemsolver_explain_fullscreen_notifications,
-            Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT,
-            ConfigUtils.isFullScreenNotificationsDisabled(ThreemaApplication.getAppContext()),
+            title = R.string.problemsolver_title_app_battery_usgae_optimized,
+            explanation = R.string.problemsolver_explain_app_battery_usgae_optimized,
+            intentAction = Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
+            check = ThreemaApplication.getServiceManager()?.preferenceService?.useThreemaPush() ?: false &&
+                !PowermanagerUtil.isIgnoringBatteryOptimizations(ThreemaApplication.getAppContext()),
         ),
         Problem(
-            R.string.problemsolver_title_app_battery_usgae_optimized,
-            R.string.problemsolver_explain_app_battery_usgae_optimized,
-            Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
-            ThreemaApplication.getServiceManager()?.preferenceService?.useThreemaPush() ?: false && !PowermanagerUtil.isIgnoringBatteryOptimizations(
-                ThreemaApplication.getAppContext(),
-            ),
+            title = R.string.problemsolver_title_android_version_deprecated,
+            explanation = R.string.problemsolver_explain_android_version_deprecated,
+            intentAction = null,
+            check = Build.VERSION.SDK_INT < Build.VERSION_CODES.N &&
+                ThreemaApplication.getServiceManager()?.preferenceService?.lastDeprecatedAndroidVersionWarningDismissed == null,
         ),
     )
 
@@ -115,36 +131,49 @@ class ProblemSolverActivity : ThreemaToolbarActivity() {
     }
 
     private fun showProblems() {
-        var problemCount = 0
         val problemsParentLayout = findViewById<LinearLayout>(R.id.problems_parent)
+        val problems = problems.filter { it.check }
         for (problem in problems) {
-            if (problem.check) {
-                val itemLayout: View = LayoutInflater.from(this).inflate(
-                    /* resource = */
-                    R.layout.item_problemsolver,
-                    /* root = */
-                    problemsParentLayout,
-                    /* attachToRoot = */
-                    false,
-                )
-                itemLayout.findViewById<TextView>(R.id.item_title).text =
-                    getString(problem.title)
-                itemLayout.findViewById<TextView>(R.id.item_explain).text =
-                    getString(problem.explanation)
-                val settingsButton = itemLayout.findViewById<MaterialButton>(R.id.item_button)
-                settingsButton.setOnClickListener { onProblemClick(problem) }
-                problemsParentLayout.addView(itemLayout)
-                problemCount++
+            val itemLayout: View = LayoutInflater.from(this).inflate(
+                /* resource = */
+                R.layout.item_problemsolver,
+                /* root = */
+                problemsParentLayout,
+                /* attachToRoot = */
+                false,
+            )
+            itemLayout.findViewById<TextView>(R.id.item_title).text =
+                getString(problem.title)
+            itemLayout.findViewById<TextView>(R.id.item_explain).text =
+                getString(problem.explanation)
+            val settingsButton = itemLayout.findViewById<MaterialButton>(R.id.item_button)
+            settingsButton.setOnClickListener { onProblemClick(problem) }
+            if (problem.intentAction == null) {
+                settingsButton.setText(R.string.ok)
+                settingsButton.icon = null
             }
+            problemsParentLayout.addView(itemLayout)
+        }
+
+        if (problems.singleOrNull()?.title == R.string.problemsolver_title_android_version_deprecated) {
+            findViewById<View>(R.id.intro_text).isVisible = false
+            findViewById<View>(R.id.info_icon).isVisible = false
+            findViewById<View>(R.id.info_text).isVisible = false
         }
 
-        if (problemCount == 0) {
+        if (problems.isEmpty()) {
             finish()
         }
     }
 
     private fun onProblemClick(problem: Problem) {
-        val action = problem.intentAction
+        if (problem.title == R.string.problemsolver_title_android_version_deprecated) {
+            ThreemaApplication.requireServiceManager().preferenceService.setLastDeprecatedAndroidVersionWarningDismissed(Instant.now())
+            recreate()
+            return
+        }
+
+        val action = problem.intentAction!!
         val intent: Intent
 
         if (problem.title == R.string.problemsolver_title_app_battery_usgae_optimized &&

+ 8 - 0
app/src/main/java/ch/threema/app/activities/ProfilePicRecipientsActivity.kt

@@ -29,10 +29,18 @@ import ch.threema.app.services.IdListService
 import ch.threema.app.tasks.ReflectUserProfileShareWithAllowListSyncTask
 import ch.threema.app.utils.LogUtil
 import ch.threema.app.utils.equalsIgnoreOrder
+import ch.threema.app.utils.logScreenVisibility
+import ch.threema.base.utils.LoggingUtil
 import ch.threema.domain.taskmanager.TaskManager
 import ch.threema.storage.models.ContactModel
 
+private val logger = LoggingUtil.getThreemaLogger("ProfilePicRecipientsActivity")
+
 class ProfilePicRecipientsActivity : MemberChooseActivity() {
+    init {
+        logScreenVisibility(logger)
+    }
+
     private lateinit var profilePicRecipientsService: IdListService
     private lateinit var taskManager: TaskManager
 

+ 7 - 0
app/src/main/java/ch/threema/app/activities/QRCodeZoomActivity.java

@@ -24,23 +24,30 @@ package ch.threema.app.activities;
 import android.os.Bundle;
 import android.view.View;
 
+import org.slf4j.Logger;
+
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.core.view.ViewCompat;
 
 import ch.threema.app.services.QRCodeServiceImpl;
 import ch.threema.app.ui.QRCodePopup;
+import ch.threema.base.utils.LoggingUtil;
 
 import static ch.threema.app.services.QRCodeServiceImpl.QR_TYPE_ANY;
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 /***
  * Activity displaying QR Code popup
  */
 public class QRCodeZoomActivity extends AppCompatActivity {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("QRCodeZoomActivity");
+
     QRCodePopup qrPopup = null;
     private static final String EXTRA_COLOR = "color";
 
     public void onCreate(final Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         final View rootView = getWindow().getDecorView().getRootView();
 

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

@@ -149,6 +149,7 @@ import static ch.threema.app.fragments.ComposeMessageFragment.MAX_FORWARDABLE_IT
 import static ch.threema.app.ui.MediaItem.TYPE_IMAGE;
 import static ch.threema.app.ui.MediaItem.TYPE_LOCATION;
 import static ch.threema.app.ui.MediaItem.TYPE_TEXT;
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
     CancelableHorizontalProgressDialog.ProgressDialogClickListener,
@@ -800,24 +801,7 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
     }
 
     private void addMediaItemSharedFromOtherApp(String mimeType, @NonNull Uri uri, @Nullable String caption) {
-        if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(uri.getScheme())) {
-            String path = uri.getPath();
-            File applicationDir = new File(getApplicationInfo().dataDir);
-
-            if (path != null) {
-                try {
-                    String inputPath = new File(path).getCanonicalPath();
-                    if (inputPath.startsWith(applicationDir.getCanonicalPath())) {
-                        Toast.makeText(this, "Illegal path", Toast.LENGTH_SHORT).show();
-                        return;
-                    }
-                } catch (IOException e) {
-                    logger.error("Exception", e);
-                    Toast.makeText(this, "Error: " + e.getMessage(), Toast.LENGTH_SHORT).show();
-                    return;
-                }
-            }
-        } else if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(uri.getScheme())) {
+        if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(uri.getScheme())) {
             try {
                 getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
             } catch (Exception e) {
@@ -827,6 +811,13 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
                 }
             }
         }
+
+        String realPath = FileUtil.getRealPathFromURI(this, uri);
+        if (realPath != null && !FileUtil.isSanePath(this, realPath)) {
+            Toast.makeText(this, R.string.invalid_file_cannot_be_shared, Toast.LENGTH_SHORT).show();
+            return;
+        }
+
         MediaItem mediaItem = new MediaItem(uri, mimeType, caption);
 
         // never create a voice message out of a shared audio file - fix default
@@ -1423,6 +1414,7 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         if (savedInstanceState != null) {
             queryText = savedInstanceState.getString(BUNDLE_QUERY_TEXT, null);

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

@@ -39,6 +39,8 @@ import ch.threema.app.utils.TestUtil;
 import ch.threema.base.utils.LoggingUtil;
 import ch.threema.domain.taskmanager.TriggerSource;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class SMSVerificationLinkActivity extends AppCompatActivity {
     private static final Logger logger = LoggingUtil.getThreemaLogger("SMSVerificationLinkActivity");
 
@@ -46,6 +48,7 @@ public class SMSVerificationLinkActivity extends AppCompatActivity {
 
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         Integer resultText = R.string.verify_failed_summary;
 

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

@@ -37,6 +37,7 @@ import static ch.threema.app.utils.MediaAdapterManagerKt.NOTIFY_ADAPTER;
 import static ch.threema.app.utils.MediaAdapterManagerKt.NOTIFY_ALL;
 import static ch.threema.app.utils.MediaAdapterManagerKt.NOTIFY_BOTH_ADAPTERS;
 import static ch.threema.app.utils.MediaAdapterManagerKt.NOTIFY_PREVIEW_ADAPTER;
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 import android.Manifest;
 import android.animation.LayoutTransition;
@@ -215,6 +216,7 @@ public class SendMediaActivity extends ThreemaToolbarActivity implements
         backgroundLayout = null;
 
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
     }
 
     @Override
@@ -487,6 +489,7 @@ public class SendMediaActivity extends ThreemaToolbarActivity implements
         sendButton.setOnClickListener(new DebouncedOnClickListener(500) {
             @Override
             public void onDebouncedClick(View v) {
+                logger.info("Send button clicked");
                 // avoid duplicates
                 v.setEnabled(false);
                 AnimationUtil.zoomOutAnimate(v);

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

@@ -41,6 +41,8 @@ import ch.threema.app.ui.ServerMessageViewModel;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.base.utils.LoggingUtil;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class ServerMessageActivity extends ThreemaActivity {
     private final static Logger logger = LoggingUtil.getThreemaLogger("ServerMessageActivity");
 
@@ -55,6 +57,7 @@ public class ServerMessageActivity extends ThreemaActivity {
         ConfigUtils.configureSystemBars(this);
 
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         final ActionBar actionBar = getSupportActionBar();
         if (actionBar != null) {

+ 10 - 1
app/src/main/java/ch/threema/app/activities/StarredMessagesActivity.kt

@@ -60,6 +60,7 @@ import ch.threema.app.ui.SelectorDialogItem
 import ch.threema.app.ui.ThreemaSearchView
 import ch.threema.app.utils.ConfigUtils
 import ch.threema.app.utils.IntentDataUtil
+import ch.threema.app.utils.logScreenVisibility
 import ch.threema.base.utils.LoggingUtil
 import ch.threema.storage.models.AbstractMessageModel
 import ch.threema.storage.models.data.DisplayTag.DISPLAY_TAG_NONE
@@ -70,11 +71,17 @@ import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
+private val logger = LoggingUtil.getThreemaLogger("StarredMessagesActivity")
+
 class StarredMessagesActivity :
     ThreemaToolbarActivity(),
     SearchView.OnQueryTextListener,
     SelectorDialog.SelectorDialogClickListener,
     GenericAlertDialog.DialogClickListener {
+    init {
+        logScreenVisibility(logger)
+    }
+
     private val starredMessagesSearchQueryTimeout = 500.milliseconds
     private var chatsAdapter: GlobalSearchAdapter? = null
     private var globalSearchViewModel: GlobalSearchViewModel? = null
@@ -178,6 +185,7 @@ class StarredMessagesActivity :
                 position: Int,
             ) {
                 if (actionMode != null) {
+                    logger.info("Starred message selection toggled")
                     chatsAdapter?.toggleChecked(position)
                     if ((chatsAdapter?.checkedItemsCount ?: 0) > 0) {
                         actionMode?.invalidate()
@@ -185,6 +193,7 @@ class StarredMessagesActivity :
                         actionMode?.finish()
                     }
                 } else {
+                    logger.info("Starred message clicked")
                     showMessage(messageModel)
                 }
             }
@@ -371,6 +380,7 @@ class StarredMessagesActivity :
 
     override fun onClick(tag: String, which: Int, data: Any?) {
         if (DIALOG_TAG_SORT_BY == tag) {
+            logger.info("Sorting order for starred messages changed")
             sortOrder = which
             preferenceService?.starredMessagesSortOrder = sortOrder
             onQueryTextChange(queryText)
@@ -393,7 +403,6 @@ class StarredMessagesActivity :
     }
 
     companion object {
-        private val logger = LoggingUtil.getThreemaLogger("StarredMessagesActivity")
         private const val DIALOG_TAG_SORT_BY = "sortBy"
         private const val FILTER_FLAGS =
             FILTER_STARRED_ONLY or FILTER_GROUPS or FILTER_CHATS or FILTER_INCLUDE_ARCHIVED

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

@@ -44,6 +44,8 @@ import ch.threema.app.R;
 import ch.threema.app.adapters.StickerSelectorAdapter;
 import ch.threema.base.utils.LoggingUtil;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class StickerSelectorActivity extends ThreemaToolbarActivity implements LoaderManager.LoaderCallbacks<String[]> {
     private static final Logger logger = LoggingUtil.getThreemaLogger("StickerSelectorActivity");
 
@@ -55,6 +57,7 @@ public class StickerSelectorActivity extends ThreemaToolbarActivity implements L
 
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         gridView = findViewById(R.id.grid_view);
         gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {

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

@@ -73,6 +73,8 @@ import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.ConversationModel;
 import ch.threema.storage.models.MessageType;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class StorageManagementActivity extends ThreemaToolbarActivity implements GenericAlertDialog.DialogClickListener, CancelableHorizontalProgressDialog.ProgressDialogClickListener {
     private static final Logger logger = LoggingUtil.getThreemaLogger("StorageManagementActivity");
 
@@ -100,6 +102,7 @@ public class StorageManagementActivity extends ThreemaToolbarActivity implements
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         ActionBar actionBar = getSupportActionBar();
         if (actionBar != null) {

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

@@ -21,6 +21,8 @@
 
 package ch.threema.app.activities;
 
+import android.os.Bundle;
+
 import org.slf4j.Logger;
 
 import java.io.UnsupportedEncodingException;
@@ -33,9 +35,17 @@ import ch.threema.app.utils.TestUtil;
 import ch.threema.app.utils.UrlUtil;
 import ch.threema.base.utils.LoggingUtil;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class SupportActivity extends SimpleWebViewActivity {
     private static final Logger logger = LoggingUtil.getThreemaLogger("SupportActivity");
 
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
+    }
+
     @Override
     protected boolean requiresConnection() {
         return true;

+ 8 - 0
app/src/main/java/ch/threema/app/activities/TermsOfServiceActivity.kt

@@ -23,8 +23,16 @@ package ch.threema.app.activities
 
 import ch.threema.app.R
 import ch.threema.app.utils.ConfigUtils
+import ch.threema.app.utils.logScreenVisibility
+import ch.threema.base.utils.LoggingUtil
+
+private val logger = LoggingUtil.getThreemaLogger("TermsOfServiceActivity")
 
 class TermsOfServiceActivity : SimpleWebViewActivity() {
+    init {
+        logScreenVisibility(logger)
+    }
+
     override fun getWebViewTitle(): Int {
         return R.string.terms_of_service
     }

+ 5 - 0
app/src/main/java/ch/threema/app/activities/ThreemaPushNotificationInfoActivity.kt

@@ -25,6 +25,7 @@ import android.os.Bundle
 import android.view.View
 import ch.threema.app.R
 import ch.threema.app.utils.ConfigUtils
+import ch.threema.app.utils.logScreenVisibility
 import ch.threema.base.utils.LoggingUtil
 
 private val logger = LoggingUtil.getThreemaLogger("ThreemaPushNotificationInfoActivity")
@@ -33,6 +34,10 @@ private val logger = LoggingUtil.getThreemaLogger("ThreemaPushNotificationInfoAc
  * Activity that is shown when the user taps on the persistent Threema Push notification.
  */
 class ThreemaPushNotificationInfoActivity : ThreemaActivity() {
+    init {
+        logScreenVisibility(logger)
+    }
+
     override fun onCreate(savedInstanceState: Bundle?) {
         logger.debug("onCreate")
 

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

@@ -58,6 +58,8 @@ import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.base.utils.LoggingUtil;
 import ch.threema.localcrypto.MasterKey;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 // Note: This should NOT extend ThreemaToolbarActivity
 public class UnlockMasterKeyActivity extends ThreemaActivity {
     private static final Logger logger = LoggingUtil.getThreemaLogger("UnlockMasterKeyActivity");
@@ -76,6 +78,7 @@ public class UnlockMasterKeyActivity extends ThreemaActivity {
         ConfigUtils.configureSystemBars(this);
 
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
 

+ 7 - 6
app/src/main/java/ch/threema/app/activities/VerificationLevelActivity.java

@@ -22,21 +22,22 @@
 package ch.threema.app.activities;
 
 import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.app.AppCompatActivity;
 import ch.threema.app.R;
-import ch.threema.app.adapters.ContactDetailAdapter;
-import ch.threema.storage.models.GroupModel;
+import ch.threema.base.utils.LoggingUtil;
 
-import android.content.Intent;
 import android.os.Bundle;
 import android.view.MenuItem;
-import android.view.View;
-import android.webkit.WebView;
+
+import org.slf4j.Logger;
+
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class VerificationLevelActivity extends ThreemaToolbarActivity {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("VerificationLevelActivity");
 
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         ActionBar actionBar = getSupportActionBar();
         if (actionBar != null) {

+ 8 - 0
app/src/main/java/ch/threema/app/activities/WhatsNewActivity.java

@@ -27,12 +27,19 @@ import android.view.View;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import org.slf4j.Logger;
+
 import ch.threema.app.BuildConfig;
 import ch.threema.app.R;
 import ch.threema.app.utils.AnimationUtil;
 import ch.threema.app.utils.ConfigUtils;
+import ch.threema.base.utils.LoggingUtil;
+
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class WhatsNewActivity extends ThreemaAppCompatActivity {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("WhatsNewActivity");
+
     public static final String EXTRA_NO_ANIMATION = "noanim";
 
     @Override
@@ -41,6 +48,7 @@ public class WhatsNewActivity extends ThreemaAppCompatActivity {
         ConfigUtils.configureSystemBars(this);
 
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         setContentView(R.layout.activity_whatsnew);
 

+ 8 - 0
app/src/main/java/ch/threema/app/activities/WorkExplainActivity.java

@@ -24,11 +24,18 @@ package ch.threema.app.activities;
 import android.content.Intent;
 import android.os.Bundle;
 
+import org.slf4j.Logger;
+
 import androidx.annotation.Nullable;
 import ch.threema.app.R;
 import ch.threema.app.utils.ConfigUtils;
+import ch.threema.base.utils.LoggingUtil;
+
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class WorkExplainActivity extends SimpleWebViewActivity {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("WorkExplainActivity");
+
     private static final String WORK_PACKAGE_NAME = "ch.threema.app.work";
 
     @Override
@@ -42,6 +49,7 @@ public class WorkExplainActivity extends SimpleWebViewActivity {
             finish();
         }
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
     }
 
     @Override

+ 12 - 0
app/src/main/java/ch/threema/app/activities/WorkIntroActivity.kt

@@ -32,8 +32,16 @@ import ch.threema.app.BuildFlavor
 import ch.threema.app.R
 import ch.threema.app.activities.wizard.components.WizardButtonXml
 import ch.threema.app.utils.LinkifyUtil
+import ch.threema.app.utils.logScreenVisibility
+import ch.threema.base.utils.LoggingUtil
+
+private val logger = LoggingUtil.getThreemaLogger("WorkIntroActivity")
 
 class WorkIntroActivity : ThreemaActivity() {
+    init {
+        logScreenVisibility(logger)
+    }
+
     companion object {
         private const val HTML_LINK_FORMAT_TEMPLATE_PREFIX = "<a href='%1\$s'> "
         private const val HTML_LINK_FORMAT_TEMPLATE_POSTFIX = " </a>"
@@ -55,6 +63,7 @@ class WorkIntroActivity : ThreemaActivity() {
         findViewById<WizardButtonXml>(R.id.work_intro_login_button_compose).apply {
             text = getString(R.string.work_intro_login)
             setOnClickListener {
+                logger.info("Login button clicked")
                 startActivity(Intent(this@WorkIntroActivity, EnterSerialActivity::class.java))
             }
         }
@@ -75,6 +84,7 @@ class WorkIntroActivity : ThreemaActivity() {
         workInfoLinkTextView.text = HtmlCompat.fromHtml(workInfoHtmlLink, HtmlCompat.FROM_HTML_MODE_COMPACT)
         workInfoLinkTextView.movementMethod = LinkMovementMethod.getInstance()
         workInfoLinkTextView.setOnClickListener {
+            logger.info("Threema Work link clicked")
             LinkifyUtil.getInstance().openLink(workInfoLink.toUri(), null, this)
         }
     }
@@ -104,6 +114,7 @@ class WorkIntroActivity : ThreemaActivity() {
     }
 
     private fun openConsumerAppInPlayStore() {
+        logger.info("Opening Play Store")
         val intent = Intent(Intent.ACTION_VIEW)
         intent.setData(getString(R.string.private_download_url).toUri())
         intent.setPackage("com.android.vending")
@@ -111,6 +122,7 @@ class WorkIntroActivity : ThreemaActivity() {
     }
 
     private fun openConsumerAppInHuaweiAppGallery() {
+        logger.info("Opening Huawai App Gallery")
         val intent = Intent(Intent.ACTION_VIEW)
         val uri = ("market://details?id=" + this.packageName).toUri()
         intent.setData(uri)

+ 3 - 0
app/src/main/java/ch/threema/app/activities/ballot/BallotChooserActivity.java

@@ -56,6 +56,8 @@ import ch.threema.app.utils.TestUtil;
 import ch.threema.base.utils.LoggingUtil;
 import ch.threema.storage.models.ballot.BallotModel;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class BallotChooserActivity extends ThreemaToolbarActivity implements ListView.OnItemClickListener {
     private static final Logger logger = LoggingUtil.getThreemaLogger("BallotChooserActivity");
 
@@ -95,6 +97,7 @@ public class BallotChooserActivity extends ThreemaToolbarActivity implements Lis
 
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         if (!this.requireInstancesOrExit()) {
             return;

+ 3 - 0
app/src/main/java/ch/threema/app/activities/ballot/BallotMatrixActivity.java

@@ -68,6 +68,8 @@ import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.ballot.BallotModel;
 import ch.threema.storage.models.ballot.BallotVoteModel;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class BallotMatrixActivity extends BallotDetailActivity {
     private static final Logger logger = LoggingUtil.getThreemaLogger("BallotMatrixActivity");
 
@@ -135,6 +137,7 @@ public class BallotMatrixActivity extends BallotDetailActivity {
 
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         if (!this.requireInstancesOrExit()) {
             return;

+ 3 - 0
app/src/main/java/ch/threema/app/activities/ballot/BallotOverviewActivity.java

@@ -70,6 +70,8 @@ import ch.threema.domain.models.MessageId;
 import ch.threema.domain.taskmanager.TriggerSource;
 import ch.threema.storage.models.ballot.BallotModel;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class BallotOverviewActivity extends ThreemaToolbarActivity implements ListView.OnItemClickListener, GenericAlertDialog.DialogClickListener, SelectorDialog.SelectorDialogClickListener {
     private static final Logger logger = LoggingUtil.getThreemaLogger("BallotOverviewActivity");
 
@@ -159,6 +161,7 @@ public class BallotOverviewActivity extends ThreemaToolbarActivity implements Li
 
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         if (!this.requireInstancesOrExit()) {
             return;

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

@@ -66,6 +66,8 @@ import ch.threema.domain.taskmanager.TriggerSource;
 import ch.threema.storage.models.ballot.BallotChoiceModel;
 import ch.threema.storage.models.ballot.BallotModel;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class BallotWizardActivity extends ThreemaActivity {
     private static final Logger logger = LoggingUtil.getThreemaLogger("BallotWizardActivity");
 
@@ -111,6 +113,7 @@ public class BallotWizardActivity extends ThreemaActivity {
         ConfigUtils.configureSystemBars(this);
 
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         setContentView(R.layout.activity_ballot_wizard);
 

+ 8 - 0
app/src/main/java/ch/threema/app/activities/ballot/BallotWizardFragment0.java

@@ -36,11 +36,18 @@ import android.widget.TextView;
 
 import com.google.android.material.textfield.TextInputLayout;
 
+import org.slf4j.Logger;
+
 import ch.threema.app.R;
 import ch.threema.app.utils.ViewUtil;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.storage.models.ballot.BallotModel;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class BallotWizardFragment0 extends BallotWizardFragment implements BallotWizardActivity.BallotWizardCallback {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("BallotWizardFragment0");
+
     private EditText editText;
     private TextInputLayout textInputLayout;
     private CheckBox secretCheckbox;
@@ -114,6 +121,7 @@ public class BallotWizardFragment0 extends BallotWizardFragment implements Ballo
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
     }
 
     @Override

+ 13 - 0
app/src/main/java/ch/threema/app/activities/ballot/BallotWizardFragment1.java

@@ -40,11 +40,14 @@ import com.google.android.material.elevation.ElevationOverlayProvider;
 import com.google.android.material.textfield.TextInputLayout;
 import com.google.android.material.timepicker.MaterialTimePicker;
 
+import org.slf4j.Logger;
+
 import java.util.Calendar;
 import java.util.Collections;
 import java.util.List;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.recyclerview.widget.ItemTouchHelper;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
@@ -54,12 +57,16 @@ import ch.threema.app.dialogs.FormatTextEntryDialog;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.EditTextUtil;
 import ch.threema.app.utils.TestUtil;
+import ch.threema.base.utils.LoggingUtil;
 import ch.threema.storage.models.ballot.BallotChoiceModel;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 import static com.google.android.material.timepicker.TimeFormat.CLOCK_12H;
 import static com.google.android.material.timepicker.TimeFormat.CLOCK_24H;
 
 public class BallotWizardFragment1 extends BallotWizardFragment implements BallotWizardActivity.BallotWizardCallback, BallotWizard1Adapter.OnChoiceListener {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("BallotWizardFragment1");
+
     private static final String DIALOG_TAG_SELECT_DATE = "selectDate";
     private static final String DIALOG_TAG_SELECT_TIME = "selectTime";
     private static final String DIALOG_TAG_SELECT_DATETIME = "selectDateTime";
@@ -75,6 +82,12 @@ public class BallotWizardFragment1 extends BallotWizardFragment implements Ballo
     private int lastVisibleBallotPosition;
     private int editItemPosition = -1;
 
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
+    }
+
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
                              Bundle savedInstanceState) {

+ 8 - 0
app/src/main/java/ch/threema/app/activities/notificationpolicy/ContactNotificationsActivity.kt

@@ -26,9 +26,17 @@ import android.view.View
 import ch.threema.app.ThreemaApplication
 import ch.threema.app.utils.ContactUtil
 import ch.threema.app.utils.TestUtil
+import ch.threema.app.utils.logScreenVisibility
+import ch.threema.base.utils.LoggingUtil
 import ch.threema.data.repositories.ContactModelRepository
 
+private val logger = LoggingUtil.getThreemaLogger("ContactNotificationsActivity")
+
 class ContactNotificationsActivity : NotificationsActivity() {
+    init {
+        logScreenVisibility(logger)
+    }
+
     private val contactModelRepository: ContactModelRepository by lazy {
         ThreemaApplication.requireServiceManager().modelRepositories.contacts
     }

+ 8 - 0
app/src/main/java/ch/threema/app/activities/notificationpolicy/GroupNotificationsActivity.kt

@@ -26,10 +26,18 @@ import android.view.View
 import androidx.annotation.UiThread
 import ch.threema.app.ThreemaApplication
 import ch.threema.app.utils.GroupUtil
+import ch.threema.app.utils.logScreenVisibility
+import ch.threema.base.utils.LoggingUtil
 import ch.threema.data.datatypes.NotificationTriggerPolicyOverride
 import ch.threema.data.repositories.GroupModelRepository
 
+private val logger = LoggingUtil.getThreemaLogger("GroupNotificationsActivity")
+
 class GroupNotificationsActivity : NotificationsActivity() {
+    init {
+        logScreenVisibility(logger)
+    }
+
     private companion object {
         const val GROUP_ID_NOT_PASSED = -1
     }

+ 32 - 12
app/src/main/java/ch/threema/app/activities/wizard/WizardBackupRestoreActivity.java

@@ -70,6 +70,8 @@ import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.base.utils.LoggingUtil;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class WizardBackupRestoreActivity extends ThreemaAppCompatActivity implements GenericAlertDialog.DialogClickListener,
     PasswordEntryDialog.PasswordEntryDialogClickListener {
     private static final Logger logger = LoggingUtil.getThreemaLogger("WizardBackupRestoreActivity");
@@ -93,12 +95,18 @@ public class WizardBackupRestoreActivity extends ThreemaAppCompatActivity implem
         registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
             // Restore backup even if permission is not granted as we do not strictly require the
             // notification permission.
+            if (isGranted) {
+                logger.info("Notification permission granted, starting restore");
+            } else {
+                logger.info("Notification permission not granted, starting restore anyway");
+            }
             startRestore();
         });
 
     @Override
     protected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         // directly forward to ID restore activity
         Intent intent = getIntent();
@@ -162,11 +170,23 @@ public class WizardBackupRestoreActivity extends ThreemaAppCompatActivity implem
         if (ConfigUtils.isWorkRestricted() && safeMDMConfig.isRestoreDisabled()) {
             safeBackupButtonCompose.setVisibility(View.GONE);
         } else {
-            safeBackupButtonCompose.setOnClickListener(v -> restoreSafe());
+            safeBackupButtonCompose.setOnClickListener(v -> {
+                logger.info("Threema Safe Backup clicked");
+                restoreSafe();
+            });
         }
-        findViewById(R.id.data_backup_compose).setOnClickListener(v -> showDisableEnergySaveDialog());
-        findViewById(R.id.id_backup_compose).setOnClickListener(v -> restoreIDExport(null, null));
-        findViewById(R.id.cancel_compose).setOnClickListener(v -> finish());
+        findViewById(R.id.data_backup_compose).setOnClickListener(v -> {
+            logger.info("Data Backup clicked");
+            showDisableEnergySaveDialog();
+        });
+        findViewById(R.id.id_backup_compose).setOnClickListener(v -> {
+            logger.info("Exported-ID clicked");
+            restoreIDExport(null, null);
+        });
+        findViewById(R.id.cancel_compose).setOnClickListener(v -> {
+            logger.info("Cancel clicked");
+            finish();
+        });
     }
 
     private void restoreSafe() {
@@ -204,9 +224,11 @@ public class WizardBackupRestoreActivity extends ThreemaAppCompatActivity implem
                     DialogUtil.dismissDialog(getSupportFragmentManager(), DIALOG_TAG_DOWNLOADING_BACKUP, true);
 
                     if (file != null) {
+                        logger.info("Backup file copied, starting restore");
                         restoreBackupFile(file);
                         file.deleteOnExit();
                     } else {
+                        logger.warn("Failed to copy backup file");
                         SimpleStringAlertDialog.newInstance(R.string.an_error_occurred, R.string.missing_permission_external_storage).show(getSupportFragmentManager(), DIALOG_TAG_ERROR_TMP_FILE_DIR);
                     }
                 });
@@ -225,10 +247,6 @@ public class WizardBackupRestoreActivity extends ThreemaAppCompatActivity implem
 
     private void restoreBackupFile(@NonNull File file) {
         if (file.exists()) {
-//			try {
-// Zipfile validity check is sometimes wrong
-//				ZipFile zipFile = new ZipFile(file);
-//				if (zipFile.isValidZipFile()) {
             ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
 
             NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
@@ -239,10 +257,6 @@ public class WizardBackupRestoreActivity extends ThreemaAppCompatActivity implem
             }
             return;
         }
-//				}
-//			} catch (ZipException e) {
-//				logger.error("Exception", e);
-//			}
         logger.error(getString(R.string.invalid_backup), this);
     }
 
@@ -276,6 +290,7 @@ public class WizardBackupRestoreActivity extends ThreemaAppCompatActivity implem
 
     @UiThread
     private void showNoInternetDialog(File file) {
+        logger.info("Showing no-internet dialog");
         GenericAlertDialog dialog = GenericAlertDialog.newInstance(R.string.menu_restore, R.string.new_wizard_need_internet, R.string.retry, R.string.cancel);
         dialog.setData(file);
         dialog.show(getSupportFragmentManager(), DIALOG_TAG_NO_INTERNET);
@@ -286,6 +301,7 @@ public class WizardBackupRestoreActivity extends ThreemaAppCompatActivity implem
     public void onYes(String tag, Object data) {
         switch (tag) {
             case DIALOG_TAG_DISABLE_ENERGYSAVE_CONFIRM:
+                logger.info("Showing disable-battery-optimizations settings");
                 Intent intent = new Intent(this, DisableBatteryOptimizationsActivity.class);
                 intent.putExtra(DisableBatteryOptimizationsActivity.EXTRA_NAME, getString(R.string.restore));
                 intent.putExtra(DisableBatteryOptimizationsActivity.EXTRA_WIZARD, true);
@@ -313,6 +329,7 @@ public class WizardBackupRestoreActivity extends ThreemaAppCompatActivity implem
 
         // If the notification permission is already granted, then start the restore directly
         if (ConfigUtils.requestNotificationPermission(this, permissionLauncher, preferenceService)) {
+            logger.info("Password was entered and permission granted, starting restore");
             startRestore();
         }
     }
@@ -337,6 +354,7 @@ public class WizardBackupRestoreActivity extends ThreemaAppCompatActivity implem
 
         switch (requestCode) {
             case REQUEST_ID_DISABLE_BATTERY_OPTIMIZATIONS:
+                logger.info("Opening restore file picker");
                 FileUtil.selectFile(WizardBackupRestoreActivity.this, null, new String[]{MimeUtil.MIME_TYPE_ZIP}, ThreemaActivity.ACTIVITY_ID_BACKUP_PICKER, false, 0, fileService.getBackupPath().getPath());
                 break;
 
@@ -355,6 +373,7 @@ public class WizardBackupRestoreActivity extends ThreemaAppCompatActivity implem
 
                         uri = resultData.getData();
                         if (uri != null) {
+                            logger.info("Restore file selected, startup backup restore");
                             restoreBackup(uri);
                         }
                     }
@@ -366,6 +385,7 @@ public class WizardBackupRestoreActivity extends ThreemaAppCompatActivity implem
 
     private void startNextWizard() {
         if (this.userService.hasIdentity()) {
+            logger.info("Starting wizard");
             this.notificationPreferenceService.setWizardRunning(true);
             startActivity(new Intent(this, WizardBaseActivity.class));
             overridePendingTransition(R.anim.abc_fade_in, R.anim.abc_fade_out);

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

@@ -89,6 +89,7 @@ import ch.threema.localcrypto.MasterKeyLockedException;
 
 import static ch.threema.app.ThreemaApplication.PHONE_LINKED_PLACEHOLDER;
 import static ch.threema.app.protocol.ApplicationSetupStepsKt.runApplicationSetupSteps;
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class WizardBaseActivity extends ThreemaAppCompatActivity implements
     LifecycleOwner,
@@ -205,6 +206,7 @@ public class WizardBaseActivity extends ThreemaAppCompatActivity implements
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         try {
             serviceManager = ThreemaApplication.getServiceManager();

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

@@ -44,6 +44,8 @@ import ch.threema.app.utils.TestUtil;
 import ch.threema.base.ThreemaException;
 import ch.threema.base.utils.LoggingUtil;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class WizardFingerPrintActivity extends WizardBackgroundActivity implements WizardDialog.WizardDialogCallback, GenericAlertDialog.DialogClickListener {
     private static final Logger logger = LoggingUtil.getThreemaLogger("WizardFingerPrintActivity");
 
@@ -57,6 +59,7 @@ public class WizardFingerPrintActivity extends WizardBackgroundActivity implemen
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
         setContentView(R.layout.activity_new_fingerprint);
 
         swipeProgress = findViewById(R.id.wizard1_swipe_progress);

+ 3 - 0
app/src/main/java/ch/threema/app/activities/wizard/WizardIDRestoreActivity.java

@@ -54,6 +54,8 @@ import ch.threema.base.utils.LoggingUtil;
 import ch.threema.domain.protocol.api.FetchIdentityException;
 import ch.threema.domain.protocol.connection.ServerConnection;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class WizardIDRestoreActivity extends WizardBackgroundActivity {
     private static final Logger logger = LoggingUtil.getThreemaLogger("WizardIDRestoreActivity");
     private static final String DIALOG_TAG_RESTORE_PROGRESS = "rp";
@@ -69,6 +71,7 @@ public class WizardIDRestoreActivity extends WizardBackgroundActivity {
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         setContentView(R.layout.activity_wizard_restore_id);
 

+ 8 - 0
app/src/main/java/ch/threema/app/activities/wizard/WizardIntroActivity.java

@@ -40,6 +40,8 @@ import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import org.slf4j.Logger;
+
 import androidx.activity.result.ActivityResultCallback;
 import androidx.activity.result.ActivityResultLauncher;
 import androidx.activity.result.contract.ActivityResultContract;
@@ -57,8 +59,13 @@ import ch.threema.app.restrictions.AppRestrictionUtil;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.app.utils.SynchronizeContactsUtil;
 import ch.threema.app.utils.TestUtil;
+import ch.threema.base.utils.LoggingUtil;
+
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class WizardIntroActivity extends WizardBackgroundActivity {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("WizardIntroActivity");
+
     private static final int ACTIVITY_RESULT_PRIVACY_POLICY = 9442;
     private AnimationDrawable frameAnimation;
 
@@ -88,6 +95,7 @@ public class WizardIntroActivity extends WizardBackgroundActivity {
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
         setContentView(R.layout.activity_wizard_intro);
 
         if (ConfigUtils.isWorkRestricted()) {

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

@@ -63,6 +63,7 @@ import ch.threema.base.utils.LoggingUtil;
 import ch.threema.domain.protocol.csp.ProtocolDefines;
 
 import static ch.threema.app.protocol.ApplicationSetupStepsKt.runApplicationSetupSteps;
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class WizardSafeRestoreActivity extends WizardBackgroundActivity implements
     PasswordEntryDialog.PasswordEntryDialogClickListener,
@@ -91,6 +92,7 @@ public class WizardSafeRestoreActivity extends WizardBackgroundActivity implemen
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         setContentView(R.layout.activity_wizard_restore_safe);
 

+ 4 - 1
app/src/main/java/ch/threema/app/activities/wizard/WizardStartActivity.java

@@ -32,6 +32,7 @@ import androidx.annotation.NonNull;
 import androidx.core.app.ActivityOptionsCompat;
 import androidx.core.app.NotificationManagerCompat;
 import androidx.core.util.Pair;
+import ch.threema.app.BuildConfig;
 import ch.threema.app.R;
 import ch.threema.app.ui.AnimationDrawableCallback;
 import ch.threema.app.utils.ConfigUtils;
@@ -39,6 +40,7 @@ import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.base.utils.LoggingUtil;
 
 import static ch.threema.app.backuprestore.csv.RestoreService.RESTORE_COMPLETION_NOTIFICATION_ID;
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class WizardStartActivity extends WizardBackgroundActivity {
     private static final Logger logger = LoggingUtil.getThreemaLogger("WizardStartActivity");
@@ -47,6 +49,7 @@ public class WizardStartActivity extends WizardBackgroundActivity {
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
         setContentView(R.layout.activity_wizard_start);
 
         NotificationManagerCompat.from(this).cancel(RESTORE_COMPLETION_NOTIFICATION_ID);
@@ -54,7 +57,7 @@ public class WizardStartActivity extends WizardBackgroundActivity {
         final ImageView imageView = findViewById(R.id.wizard_animation);
         final AnimationDrawable frameAnimation = getAnimationDrawable(imageView);
 
-        if (!RuntimeUtil.isInTest() && !ConfigUtils.isWorkRestricted()) {
+        if (!RuntimeUtil.isInTest() && !ConfigUtils.isWorkRestricted() && !BuildConfig.DEBUG) {
             imageView.setOnClickListener(v -> {
                 ((AnimationDrawable) v.getBackground()).stop();
                 launchNextActivity(null);

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

@@ -21,6 +21,7 @@
 
 package ch.threema.app.adapters;
 
+import static ch.threema.app.utils.MessageUtilKt.findIndexByMessageId;
 import static ch.threema.domain.protocol.csp.messages.file.FileData.RENDERING_DEFAULT;
 
 import android.content.Context;
@@ -1162,29 +1163,19 @@ public class ComposeMessageAdapter extends ArrayAdapter<AbstractMessageModel> im
      * Get adapter position of next available (i.e. downloaded) voice message with same incoming/outgoing status
      *
      * @param messageModel of original message
-     * @return AbstractMessageModel of next message in adapter that matches the specified criteria or AbsListView.INVALID_POSITION if none is found
+     * @return Position of the next message in the adapter that matches the specified criteria or AbsListView.INVALID_POSITION if none is found
      */
-    public int getNextVoiceMessage(AbstractMessageModel messageModel) {
-        int index = values.indexOf(messageModel);
-        if (index < values.size() - 1) {
+    public int getNextVoiceMessage(@NonNull AbstractMessageModel messageModel) {
+        Integer index = findIndexByMessageId(values, messageModel.getId());
+        if (index != null && index < values.size() - 1) {
             AbstractMessageModel nextMessage = values.get(index + 1);
-            if (nextMessage != null) {
-                boolean isVoiceMessage = nextMessage.getType() == MessageType.VOICEMESSAGE;
-                if (!isVoiceMessage) {
-                    // new school voice messages
-                    isVoiceMessage = nextMessage.getType() == MessageType.FILE &&
-                        MimeUtil.isAudioFile(nextMessage.getFileData().getMimeType()) &&
-                        nextMessage.getFileData().getRenderingType() == FileData.RENDERING_MEDIA &&
-                        nextMessage.getFileData().isDownloaded();
-                }
-
-                if (isVoiceMessage) {
-                    if (messageModel.isOutbox() == nextMessage.isOutbox()) {
-                        if (messageModel.isAvailable()) {
-                            return index + 1;
-                        }
-                    }
-                }
+            if (
+                nextMessage != null &&
+                    nextMessage.isDownloadedVoiceMessage() &&
+                    messageModel.isOutbox() == nextMessage.isOutbox() &&
+                    messageModel.isAvailable()
+            ) {
+                return index + 1;
             }
         }
         return AbsListView.INVALID_POSITION;

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

@@ -568,8 +568,6 @@ public class GroupDetailAdapter extends RecyclerView.Adapter<RecyclerView.ViewHo
     }
 
     public interface OnGroupDetailsClickListener {
-        void onGroupOwnerClick(View v, String identity);
-
         void onGroupMemberClick(View v, @NonNull ContactModel contactModel);
 
         void onResetLinkClick();

+ 1 - 0
app/src/main/java/ch/threema/app/adapters/MessageListViewHolder.kt

@@ -205,6 +205,7 @@ class MessageListViewHolder(
                 // position may have changed after the item was bound. query current position from holder
                 val currentPos = layoutPosition
                 if (currentPos >= 0) {
+                    logger.info("Message clicked")
                     clickListener.onItemClick(v, currentPos)
                 }
             }

+ 55 - 60
app/src/main/java/ch/threema/app/adapters/WidgetViewsFactory.java

@@ -21,7 +21,6 @@
 
 package ch.threema.app.adapters;
 
-import android.appwidget.AppWidgetManager;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
@@ -33,6 +32,8 @@ import org.slf4j.Logger;
 
 import java.util.List;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.managers.ServiceManager;
@@ -54,41 +55,45 @@ import ch.threema.storage.models.ConversationModel;
 public class WidgetViewsFactory implements RemoteViewsService.RemoteViewsFactory {
     private static final Logger logger = LoggingUtil.getThreemaLogger("WidgetViewsFactory");
 
-    private Context context;
-    private int appWidgetId;
-    private ServiceManager serviceManager;
-    private ConversationService conversationService;
-    private GroupService groupService;
-    private ContactService contactService;
-    private DistributionListService distributionListService;
-    private LockAppService lockAppService;
-    private PreferenceService preferenceService;
-    private NotificationPreferenceService notificationPreferenceService;
-    private MessageService messageService;
-    private ConversationCategoryService conversationCategoryService;
+    @NonNull
+    private final Context context;
+    @NonNull
+    private final ConversationService conversationService;
+    @NonNull
+    private final GroupService groupService;
+    @NonNull
+    private final ContactService contactService;
+    @NonNull
+    private final DistributionListService distributionListService;
+    @NonNull
+    private final LockAppService lockAppService;
+    @NonNull
+    private final PreferenceService preferenceService;
+    @NonNull
+    private final NotificationPreferenceService notificationPreferenceService;
+    @NonNull
+    private final MessageService messageService;
+    @NonNull
+    private final ConversationCategoryService conversationCategoryService;
+
     private List<ConversationModel> conversations;
 
-    public WidgetViewsFactory(Context context, Intent intent) {
+    public WidgetViewsFactory(@NonNull Context context) throws ThreemaException {
         this.context = context;
-        this.appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
-            AppWidgetManager.INVALID_APPWIDGET_ID);
 
-        this.serviceManager = ThreemaApplication.getServiceManager();
-        if (this.serviceManager != null) {
-            try {
-                this.conversationService = serviceManager.getConversationService();
-                this.contactService = serviceManager.getContactService();
-                this.groupService = serviceManager.getGroupService();
-                this.distributionListService = serviceManager.getDistributionListService();
-                this.messageService = serviceManager.getMessageService();
-                this.lockAppService = serviceManager.getLockAppService();
-                this.preferenceService = serviceManager.getPreferenceService();
-                this.notificationPreferenceService = serviceManager.getNotificationPreferenceService();
-                this.conversationCategoryService = serviceManager.getConversationCategoryService();
-            } catch (ThreemaException e) {
-                logger.debug("no conversationservice");
-            }
+        ServiceManager serviceManager = ThreemaApplication.getServiceManager();
+        if (serviceManager == null) {
+            throw new ThreemaException("Could not get the service manager");
         }
+        this.conversationService = serviceManager.getConversationService();
+        this.contactService = serviceManager.getContactService();
+        this.groupService = serviceManager.getGroupService();
+        this.distributionListService = serviceManager.getDistributionListService();
+        this.messageService = serviceManager.getMessageService();
+        this.lockAppService = serviceManager.getLockAppService();
+        this.preferenceService = serviceManager.getPreferenceService();
+        this.notificationPreferenceService = serviceManager.getNotificationPreferenceService();
+        this.conversationCategoryService = serviceManager.getConversationCategoryService();
     }
 
 
@@ -114,30 +119,19 @@ public class WidgetViewsFactory implements RemoteViewsService.RemoteViewsFactory
      */
     @Override
     public void onDataSetChanged() {
-        if (contactService != null) {
-            conversations = conversationService.getAll(false, new ConversationService.Filter() {
-                @Override
-                public boolean onlyUnread() {
-                    return true;
-                }
-
-                @Override
-                public boolean noDistributionLists() {
-                    return false;
-                }
-
-                @Override
-                public boolean noHiddenChats() {
-                    return preferenceService.isPrivateChatsHidden();
-                }
+        conversations = conversationService.getAll(false, new ConversationService.Filter() {
+            @Override
+            public boolean onlyUnread() {
+                return true;
+            }
 
-                @Override
-                public boolean noInvalid() {
-                    return false;
-                }
+            @Override
+            public boolean noHiddenChats() {
+                return preferenceService.isPrivateChatsHidden();
+            }
 
-            });
-        }
+        });
+        logger.info("Conversations updated");
     }
 
     /**
@@ -154,10 +148,10 @@ public class WidgetViewsFactory implements RemoteViewsService.RemoteViewsFactory
      */
     @Override
     public int getCount() {
-        if (lockAppService != null &&
-            !lockAppService.isLocked() &&
+        if (!lockAppService.isLocked() &&
             notificationPreferenceService.isShowMessagePreview() &&
-            conversations != null) {
+            conversations != null
+        ) {
             return conversations.size();
         } else {
             return 0;
@@ -174,16 +168,17 @@ public class WidgetViewsFactory implements RemoteViewsService.RemoteViewsFactory
      */
     @Override
     public RemoteViews getViewAt(int position) {
-        if (conversations != null && conversations.size() > 0 && position < conversations.size()) {
+        if (conversations != null && !conversations.isEmpty() && position < conversations.size()) {
             ConversationModel conversationModel = conversations.get(position);
 
             if (conversationModel != null) {
-                String sender = "", message = "", date = "", count = "";
+                @Nullable String message = "";
+                String sender = "", date = "", count = "";
                 Bitmap avatar = null;
                 Bundle extras = new Bundle();
                 String uniqueId = conversationModel.getReceiver().getUniqueIdString();
 
-                if (this.lockAppService != null && !this.lockAppService.isLocked() && notificationPreferenceService.isShowMessagePreview()) {
+                if (!this.lockAppService.isLocked() && notificationPreferenceService.isShowMessagePreview()) {
                     sender = conversationModel.getReceiver().getDisplayName();
 
                     if (conversationModel.isContactConversation()) {
@@ -199,7 +194,7 @@ public class WidgetViewsFactory implements RemoteViewsService.RemoteViewsFactory
 
                     count = Long.toString(conversationModel.getUnreadCount());
 
-                    if (conversationCategoryService != null && conversationCategoryService.isPrivateChat(uniqueId)) {
+                    if (conversationCategoryService.isPrivateChat(uniqueId)) {
                         message = context.getString(R.string.private_chat_subject);
                     } else if (conversationModel.getLatestMessage() != null) {
                         AbstractMessageModel messageModel = conversationModel.getLatestMessage();

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

@@ -75,6 +75,7 @@ import java8.util.stream.StreamSupport;
 
 import static ch.threema.app.managers.ListenerManager.conversationListeners;
 import static ch.threema.app.managers.ListenerManager.messageListeners;
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class ArchiveActivity extends ThreemaToolbarActivity implements GenericAlertDialog.DialogClickListener, SearchView.OnQueryTextListener {
     private static final Logger logger = LoggingUtil.getThreemaLogger("ArchiveActivity");
@@ -100,6 +101,7 @@ public class ArchiveActivity extends ThreemaToolbarActivity implements GenericAl
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         conversationListeners.add(this.conversationListener);
         messageListeners.add(this.messageListener);

+ 73 - 0
app/src/main/java/ch/threema/app/backuprestore/ZipFileWrapper.kt

@@ -0,0 +1,73 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2025 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.backuprestore
+
+import java.io.File
+import java.io.IOException
+import java.io.InputStream
+import net.lingala.zip4j.ZipFile
+import net.lingala.zip4j.exception.ZipException
+import net.lingala.zip4j.model.FileHeader
+import org.apache.commons.io.input.ProxyInputStream
+
+/**
+ * The [ZipFile] class has the problem that whenever [ZipFile.getInputStream] is called, it keeps a reference to the created input stream internally.
+ * This is essentially a memory-leak, as these references aren't needed for anything other than being able to close all streams when calling
+ * [ZipFile.close], which in our case is unnecessary as we already close them pro-actively. The [ZipFileWrapper] class comes in as a
+ * helper to this problem, by ensuring that [ZipFile.close] is called pro-actively as well, making it remove the unnecessary internal reference.
+ * The [ZipFile] instance can still be used normally afterwards.
+ */
+class ZipFileWrapper(
+    file: File,
+    password: String,
+) {
+    private val zipFile = ZipFile(file, password.toCharArray())
+
+    fun isValidZipFile() =
+        zipFile.isValidZipFile
+
+    @Throws(ZipException::class)
+    fun getFileHeaders(): List<FileHeader> =
+        zipFile.fileHeaders
+
+    fun getInputStream(fileHeader: FileHeader): InputStream =
+        CloseCallbackInputStream(
+            inputStream = zipFile.getInputStream(fileHeader),
+            onClosed = {
+                try {
+                    zipFile.close()
+                } catch (e: IOException) {
+                    // should never happen, but if it does, we'd rather ignore it than cause a scene
+                }
+            },
+        )
+
+    private class CloseCallbackInputStream(
+        inputStream: InputStream,
+        private val onClosed: () -> Unit,
+    ) : ProxyInputStream(inputStream) {
+        override fun close() {
+            super.close()
+            onClosed()
+        }
+    }
+}

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

@@ -22,9 +22,11 @@
 package ch.threema.app.backuprestore.csv;
 
 import android.annotation.SuppressLint;
+import android.app.ActivityManager;
 import android.app.Notification;
 import android.app.PendingIntent;
 import android.app.Service;
+import android.content.ComponentCallbacks2;
 import android.content.Context;
 import android.content.Intent;
 import android.os.AsyncTask;
@@ -34,6 +36,7 @@ import android.os.PowerManager;
 import android.os.StrictMode;
 import android.text.TextUtils;
 import android.text.format.DateUtils;
+import android.text.format.Formatter;
 import android.widget.Toast;
 
 import net.lingala.zip4j.ZipFile;
@@ -49,6 +52,8 @@ import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.net.UnknownHostException;
 import java.nio.charset.Charset;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
@@ -71,6 +76,7 @@ import ch.threema.app.activities.DummyActivity;
 import ch.threema.app.activities.HomeActivity;
 import ch.threema.app.asynctasks.DeleteIdentityAsyncTask;
 import ch.threema.app.backuprestore.MessageIdCache;
+import ch.threema.app.backuprestore.ZipFileWrapper;
 import ch.threema.app.collections.Functional;
 import ch.threema.app.emojis.EmojiUtil;
 import ch.threema.app.exceptions.RestoreCanceledException;
@@ -140,7 +146,7 @@ import static ch.threema.app.utils.IntentDataUtil.PENDING_INTENT_FLAG_IMMUTABLE;
 import static ch.threema.storage.models.GroupModel.UserState.LEFT;
 import static ch.threema.storage.models.GroupModel.UserState.MEMBER;
 
-public class RestoreService extends Service {
+public class RestoreService extends Service implements ComponentCallbacks2 {
     private static final Logger logger = LoggingUtil.getThreemaLogger("RestoreService");
 
     public static final String RESTORE_PROGRESS_INTENT = "restore_progress_intent";
@@ -166,6 +172,7 @@ public class RestoreService extends Service {
     private NotificationPreferenceService notificationPreferenceService;
     private PowerManager.WakeLock wakeLock;
     private NotificationManagerCompat notificationManagerCompat;
+    private ActivityManager activityManager;
     private NonceFactory nonceFactory;
     private @NonNull GroupModelRepository groupModelRepository;
 
@@ -185,7 +192,7 @@ public class RestoreService extends Service {
 
     private static boolean restoreSuccess = false;
 
-    private ZipFile zipFile;
+    private ZipFileWrapper zipFileWrapper;
     private String password;
 
     private static final int STEP_SIZE_PREPARE = 100;
@@ -267,12 +274,10 @@ public class RestoreService extends Service {
                 new AsyncTask<Void, Void, Boolean>() {
                     @Override
                     protected Boolean doInBackground(Void... params) {
-                        zipFile = new ZipFile(file, password.toCharArray());
-                        if (!zipFile.isValidZipFile()) {
-                            showRestoreErrorNotification(getString(R.string.restore_zip_invalid_file));
-                            isRunning = false;
-
-                            return false;
+                        logger.info("Size of backup file: {}", formatFileSize(file.length()));
+                        zipFileWrapper = new ZipFileWrapper(file, password);
+                        if (!zipFileWrapper.isValidZipFile()) {
+                            logger.warn("ZIP file might be invalid, restore might fail");
                         }
                         return restore();
                     }
@@ -290,9 +295,11 @@ public class RestoreService extends Service {
                 Toast.makeText(this, R.string.restore_data_cancelled, Toast.LENGTH_LONG).show();
             }
         } else {
-            logger.debug("onStartCommand intent == null");
-
-            onFinished("Empty intent");
+            logger.warn("onStartCommand intent == null");
+            // The term "empty-intent" isn't meaningful to the user, but it is so infamous
+            // that it is known internally (to support and devs), which is why we keep it
+            // in the error message, allowing us to more easily identify it in bug reports.
+            onFinished(getString(R.string.restore_error_body) + " (error code: empty-intent)");
         }
         isRunning = false;
 
@@ -331,6 +338,7 @@ public class RestoreService extends Service {
         }
 
         notificationManagerCompat = NotificationManagerCompat.from(this);
+        activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
     }
 
     @Override
@@ -346,8 +354,27 @@ public class RestoreService extends Service {
 
     @Override
     public void onLowMemory() {
-        logger.info("onLowMemory");
-        super.onLowMemory();
+        logger.warn("onLowMemory");
+    }
+
+    @Override
+    public void onTrimMemory(int level) {
+        logger.warn("onTrimMemory({})", level);
+        logMemoryStatus();
+    }
+
+    private void logMemoryStatus() {
+        if (activityManager != null) {
+            ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
+            activityManager.getMemoryInfo(memoryInfo);
+            logger.info(
+                "Memory status: available={}, total={}, threshold={}, low={}",
+                formatFileSize(memoryInfo.availMem),
+                formatFileSize(memoryInfo.totalMem),
+                formatFileSize(memoryInfo.threshold),
+                memoryInfo.lowMemory
+            );
+        }
     }
 
     @Override
@@ -410,6 +437,7 @@ public class RestoreService extends Service {
             // but does not write to the database. In the second pass, the files are actually written.
             for (int nTry = 0; nTry < 2; nTry++) {
                 logger.info("Attempt {}", nTry + 1);
+                logMemoryStatus();
                 if (nTry > 0) {
                     this.writeToDb = true;
                     this.initProgress(stepSizeTotal);
@@ -447,7 +475,7 @@ public class RestoreService extends Service {
                     fileService.clearDirectory(fileService.getAppDataPath(), false);
                 }
 
-                List<FileHeader> fileHeaders = zipFile.getFileHeaders();
+                List<FileHeader> fileHeaders = zipFileWrapper.getFileHeaders();
 
                 // The restore settings file contains the data backup format version
                 this.restoreSettings = getRestoreSettings(fileHeaders);
@@ -469,7 +497,7 @@ public class RestoreService extends Service {
                 );
                 if (identityHeader != null && this.writeToDb) {
                     String identityContent;
-                    try (InputStream inputStream = zipFile.getInputStream(identityHeader)) {
+                    try (InputStream inputStream = getZipFileInputStream(identityHeader)) {
                         identityContent = IOUtils.toString(inputStream, Charset.defaultCharset());
                     }
 
@@ -520,6 +548,8 @@ public class RestoreService extends Service {
 
                 updateProgress(STEP_SIZE_GROUP_AVATARS);
 
+                // The media files are the most memory hungry, so we log the memory status before and after
+                logMemoryStatus();
                 logger.info("Restoring message media files");
                 long mediaCount = this.restoreMessageMediaFiles(fileHeaders);
                 if (mediaCount == 0) {
@@ -528,6 +558,9 @@ public class RestoreService extends Service {
                 } else {
                     logger.info("{} media files found", mediaCount);
                 }
+                if (writeToDb) {
+                    logMemoryStatus();
+                }
 
                 // restore all avatars
                 logger.info("Restoring avatars");
@@ -590,7 +623,7 @@ public class RestoreService extends Service {
             throw new ThreemaException(getString(R.string.invalid_backup));
         }
         try (
-            InputStream is = zipFile.getInputStream(settingsHeader);
+            InputStream is = getZipFileInputStream(settingsHeader);
             InputStreamReader inputStreamReader = new InputStreamReader(is);
             CSVReader csvReader = new CSVReader(inputStreamReader, false)
         ) {
@@ -701,7 +734,7 @@ public class RestoreService extends Service {
             logger.info("No nonce count file available in backup");
             return -1;
         }
-        try (ZipInputStream inputStream = this.zipFile.getInputStream(nonceCountFileHeader);
+        try (InputStream inputStream = getZipFileInputStream(nonceCountFileHeader);
              InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
              CSVReader csvReader = new CSVReader(inputStreamReader, true)
         ) {
@@ -747,7 +780,7 @@ public class RestoreService extends Service {
             return 0;
         }
 
-        try (ZipInputStream inputStream = this.zipFile.getInputStream(nonceFileHeader);
+        try (InputStream inputStream = getZipFileInputStream(nonceFileHeader);
              InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
              CSVReader csvReader = new CSVReader(inputStreamReader, true)
         ) {
@@ -771,13 +804,18 @@ public class RestoreService extends Service {
                             }
                         }
                     }
-                } catch (ThreemaException e) {
+                } catch (ThreemaException|NoSuchAlgorithmException|InvalidKeyException e) {
                     logger.error("Could not insert nonces", e);
                     return 0;
                 }
             }
             if (!nonceBytes.isEmpty()) {
-                success &= insertNonces(scope, nonceBytes);
+                try {
+                    success &= insertNonces(scope, nonceBytes);
+                } catch (NoSuchAlgorithmException|InvalidKeyException e) {
+                    logger.error("Could not insert remaining nonces", e);
+                    success = false;
+                }
             }
             if (success) {
                 logger.info("Restored {} {} nonces", nonceCount, scope);
@@ -792,9 +830,9 @@ public class RestoreService extends Service {
     private boolean insertNonces(
         @NonNull NonceScope scope,
         @NonNull List<byte[]> nonces
-    ) throws RestoreCanceledException {
+    ) throws RestoreCanceledException, NoSuchAlgorithmException, InvalidKeyException {
         logger.debug("Write {} nonces to database", nonces.size());
-        boolean success = nonceFactory.insertHashedNoncesJava(scope, nonces);
+        boolean success = nonceFactory.insertHashedNoncesJava(scope, nonces, userService.getIdentity());
         updateProgress(nonces.size() / NONCES_PER_STEP);
         return success;
     }
@@ -827,7 +865,7 @@ public class RestoreService extends Service {
      * progress calculation of the restore process.
      */
     private long getRestoreReactionsSteps(@NonNull FileHeader reactionCountFileHeader) throws IOException {
-        try (ZipInputStream inputStream = this.zipFile.getInputStream(reactionCountFileHeader);
+        try (InputStream inputStream = getZipFileInputStream(reactionCountFileHeader);
              InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
              CSVReader csvReader = new CSVReader(inputStreamReader, true)
         ) {
@@ -877,7 +915,7 @@ public class RestoreService extends Service {
     }
 
     private void iterateRows(@NonNull FileHeader fileHeader, ThrowingConsumer<CSVRow> rowConsumer) throws Exception {
-        try (ZipInputStream inputStream = this.zipFile.getInputStream(fileHeader);
+        try (InputStream inputStream = getZipFileInputStream(fileHeader);
              InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
              CSVReader csvReader = new CSVReader(inputStreamReader, true)
         ) {
@@ -1052,7 +1090,7 @@ public class RestoreService extends Service {
                 if (groupId != null) {
                     ch.threema.data.models.GroupModel m = groupModelRepository.getByLocalGroupDbId(groupId);
                     if (m != null) {
-                        try (InputStream inputStream = zipFile.getInputStream(fileHeader)) {
+                        try (InputStream inputStream = getZipFileInputStream(fileHeader)) {
                             this.fileService.writeGroupAvatar(m, inputStream);
                         } catch (Exception e) {
                             //ignore, just the avatar :)
@@ -1110,12 +1148,12 @@ public class RestoreService extends Service {
 
         // process all thumbnails
         Map<String, FileHeader> thumbnailFileHeaders = new HashMap<>();
-
-        for (FileHeader fileHeader : fileHeaders) {
-            String fileName = fileHeader.getFileName();
-            if (!TestUtil.isEmptyOrNull(fileName)
-                && fileName.startsWith(thumbnailPrefix)) {
-                thumbnailFileHeaders.put(fileName, fileHeader);
+        if (writeToDb) {
+            for (FileHeader fileHeader : fileHeaders) {
+                String fileName = fileHeader.getFileName();
+                if (!TestUtil.isEmptyOrNull(fileName) && fileName.startsWith(thumbnailPrefix)) {
+                    thumbnailFileHeaders.put(fileName, fileHeader);
+                }
             }
         }
 
@@ -1140,31 +1178,34 @@ public class RestoreService extends Service {
                             // restore thumbnail
                             FileHeader thumbnailFileHeader = thumbnailFileHeaders.get(thumbnailPrefix + messageUid);
                             if (thumbnailFileHeader != null) {
-                                logger.info("Restoring thumbnail from file");
                                 long fileSize = thumbnailFileHeader.getUncompressedSize();
+                                logger.info("Restoring thumbnail from file ({})", formatFileSize(fileSize));
                                 if (fileSize < MAX_THUMBNAIL_SIZE_BYTES) {
-                                    try (InputStream inputStream = zipFile.getInputStream(thumbnailFileHeader)) {
+                                    try (InputStream inputStream = getZipFileInputStream(thumbnailFileHeader)) {
                                         this.fileService.saveThumbnail(model, inputStream);
                                     }
                                 }
                             }
                         } else {
-                            logger.info("Restoring media from file, with message contents type = {}", model.getMessageContentsType());
-                            try (ZipInputStream inputStream = zipFile.getInputStream(fileHeader)) {
+                            logger.info(
+                                "Restoring media from file, with message contents type = {}, {}",
+                                model.getMessageContentsType(),
+                                formatFileSize(fileHeader.getUncompressedSize())
+                            );
+                            try (InputStream inputStream = getZipFileInputStream(fileHeader)) {
                                 this.fileService.writeConversationMedia(model, inputStream);
                             }
 
-                            // TODO(ANDR-3737): The following check is insufficient and leads to false positives,
-                            //  e.g. regular PDFs or text files are also passed through the thumbnail generation, even though that will
-                            //  always fails. It also does not handle video thumbnails properly.
-                            if (MessageUtil.canHaveThumbnailFile(model)) {
-                                // check if a thumbnail file is in backup
+                            // TODO(ANDR-3737): The following check is insufficient and leads to false positives and false negatives,
+                            //  e.g. video files are not handled properly
+                            if (MessageUtil.canHaveThumbnailFile(model) && model.getMessageContentsType() == MessageContentsType.IMAGE) {
+                                    // check if a thumbnail file is in backup
                                 FileHeader thumbnailFileHeader = thumbnailFileHeaders.get(thumbnailPrefix + messageUid);
 
                                 // if no thumbnail file exist in backup, generate one
                                 if (thumbnailFileHeader == null) {
-                                    logger.info("Generating thumbnail for media file ");
-                                    try (ResettableInputStream inputStream = new ResettableInputStream(() -> zipFile.getInputStream(fileHeader))) {
+                                    logger.info("Generating thumbnail for media file");
+                                    try (ResettableInputStream inputStream = new ResettableInputStream(() -> getZipFileInputStream(fileHeader))) {
                                         fileService.writeConversationMediaThumbnail(model, inputStream);
                                     } catch (Exception e) {
                                         logger.warn("Failed to generate thumbnail for media file, skipping", e);
@@ -1228,7 +1269,7 @@ public class RestoreService extends Service {
         }
 
         // Set contact avatar
-        try (ZipInputStream inputStream = zipFile.getInputStream(fileHeader)) {
+        try (InputStream inputStream = getZipFileInputStream(fileHeader)) {
             return fileService.writeUserDefinedProfilePicture(contactModel.getIdentity(), inputStream);
         } catch (Exception e) {
             logger.error("Exception while writing contact avatar", e);
@@ -1254,7 +1295,7 @@ public class RestoreService extends Service {
         }
 
         // Set contact profile picture
-        try (ZipInputStream inputStream = zipFile.getInputStream(fileHeader)) {
+        try (InputStream inputStream = getZipFileInputStream(fileHeader)) {
             return fileService.writeContactDefinedProfilePicture(contactModel.getIdentity(), inputStream);
         } catch (Exception e) {
             logger.error("Exception while writing contact profile picture", e);
@@ -2100,7 +2141,7 @@ public class RestoreService extends Service {
         @NonNull FileHeader fileHeader,
         @NonNull ProcessCsvFile processCsvFile
     ) throws IOException, RestoreCanceledException {
-        try (ZipInputStream inputStream = this.zipFile.getInputStream(fileHeader);
+        try (InputStream inputStream = getZipFileInputStream(fileHeader);
              InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
              CSVReader csvReader = new CSVReader(inputStreamReader, true)) {
             CSVRow row;
@@ -2111,6 +2152,14 @@ public class RestoreService extends Service {
         return true;
     }
 
+    private InputStream getZipFileInputStream(FileHeader fileHeader) {
+        return zipFileWrapper.getInputStream(fileHeader);
+    }
+
+    private String formatFileSize(long sizeBytes) {
+        return Formatter.formatFileSize(this, sizeBytes);
+    }
+
     private void initProgress(long steps) {
         this.currentProgressStep = 0;
         this.progressSteps = steps;

+ 2 - 6
app/src/main/java/ch/threema/app/camera/CameraActivity.java

@@ -21,7 +21,6 @@
 
 package ch.threema.app.camera;
 
-import android.annotation.SuppressLint;
 import android.content.Intent;
 import android.graphics.Color;
 import android.os.Build;
@@ -32,24 +31,20 @@ import android.view.View;
 import android.view.WindowManager;
 
 import androidx.annotation.Nullable;
-import androidx.camera.lifecycle.ProcessCameraProvider;
-import androidx.core.content.ContextCompat;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentManager;
 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
 
-import com.google.common.util.concurrent.ListenableFuture;
-
 import org.slf4j.Logger;
 
 import ch.threema.app.R;
-import ch.threema.app.ThreemaApplication;
 import ch.threema.app.activities.ThreemaAppCompatActivity;
 import ch.threema.app.utils.ConfigUtils;
 import ch.threema.base.utils.LoggingUtil;
 
 import static android.view.KeyEvent.KEYCODE_VOLUME_DOWN;
 import static android.view.KeyEvent.KEYCODE_VOLUME_UP;
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class CameraActivity extends ThreemaAppCompatActivity implements CameraFragment.CameraCallback, CameraFragment.CameraConfiguration {
     private static final Logger logger = LoggingUtil.getThreemaLogger("CameraActivity");
@@ -70,6 +65,7 @@ public class CameraActivity extends ThreemaAppCompatActivity implements CameraFr
         logger.info("onCreate");
 
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         setContentView(R.layout.camerax_activity_camera);
 

+ 6 - 1
app/src/main/java/ch/threema/app/camera/CameraFragment.kt

@@ -85,6 +85,7 @@ import ch.threema.app.ui.LessObnoxiousMediaActionSound
 import ch.threema.app.utils.ConfigUtils
 import ch.threema.app.utils.LocaleUtil
 import ch.threema.app.utils.RuntimeUtil
+import ch.threema.app.utils.logScreenVisibility
 import ch.threema.base.utils.LoggingUtil
 import com.google.android.material.progressindicator.CircularProgressIndicator
 import java.io.File
@@ -98,8 +99,12 @@ import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.takeWhile
 import kotlinx.coroutines.launch
 
+private val logger = LoggingUtil.getThreemaLogger("CameraFragment")
+
 class CameraFragment : Fragment() {
-    private val logger = LoggingUtil.getThreemaLogger("CameraFragment")
+    init {
+        logScreenVisibility(logger)
+    }
 
     private lateinit var viewModel: CameraFragmentViewModel
 

+ 7 - 2
app/src/main/java/ch/threema/app/camera/QRScannerActivity.kt

@@ -48,18 +48,23 @@ import ch.threema.app.activities.ThreemaActivity
 import ch.threema.app.services.QRCodeServiceImpl.QRCodeColor
 import ch.threema.app.services.QRCodeServiceImpl.QR_TYPE_ANY
 import ch.threema.app.utils.SoundUtil
+import ch.threema.app.utils.logScreenVisibility
 import ch.threema.base.utils.LoggingUtil
 import java.util.concurrent.ExecutorService
 import java.util.concurrent.Executors
 
+private val logger = LoggingUtil.getThreemaLogger("QRScannerActivity")
+
 class QRScannerActivity : ThreemaActivity() {
+    init {
+        logScreenVisibility(logger)
+    }
+
     companion object {
         const val KEY_HINT_TEXT: String = "hint"
         const val KEY_QR_TYPE: String = "qrType"
     }
 
-    private val logger = LoggingUtil.getThreemaLogger("QRScannerActivity")
-
     private lateinit var cameraExecutor: ExecutorService
 
     private lateinit var cameraPreview: PreviewView

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

@@ -60,6 +60,8 @@ import ch.threema.storage.models.ballot.BallotChoiceModel;
 import ch.threema.storage.models.ballot.BallotModel;
 import ch.threema.storage.models.ballot.BallotVoteModel;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
+
 public class BallotVoteDialog extends ThreemaDialogFragment {
     private static final Logger logger = LoggingUtil.getThreemaLogger("BallotVoteDialog");
 
@@ -139,6 +141,7 @@ public class BallotVoteDialog extends ThreemaDialogFragment {
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         ListenerManager.ballotListeners.add(this.ballotListener);
         ListenerManager.ballotVoteListeners.add(this.ballotVoteListener);

+ 13 - 0
app/src/main/java/ch/threema/app/dialogs/BottomSheetGridDialog.java

@@ -23,12 +23,19 @@ package ch.threema.app.dialogs;
 
 import android.os.Bundle;
 
+import org.slf4j.Logger;
+
 import java.util.ArrayList;
 
 import androidx.annotation.StringRes;
 import ch.threema.app.ui.BottomSheetItem;
+import ch.threema.base.utils.LoggingUtil;
+
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class BottomSheetGridDialog extends BottomSheetAbstractDialog {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("BottomSheetGridDialog");
+
     public static BottomSheetGridDialog newInstance(@StringRes int title, ArrayList<BottomSheetItem> items) {
         BottomSheetGridDialog dialog = new BottomSheetGridDialog();
         Bundle args = new Bundle();
@@ -38,6 +45,12 @@ public class BottomSheetGridDialog extends BottomSheetAbstractDialog {
         return dialog;
     }
 
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
+    }
+
     /* Hack to prevent TransactionTooLargeException when hosting activity goes into the background */
     @Override
     public void onPause() {

+ 13 - 0
app/src/main/java/ch/threema/app/dialogs/BottomSheetListDialog.java

@@ -23,12 +23,19 @@ package ch.threema.app.dialogs;
 
 import android.os.Bundle;
 
+import org.slf4j.Logger;
+
 import java.util.ArrayList;
 
 import androidx.annotation.StringRes;
 import ch.threema.app.ui.BottomSheetItem;
+import ch.threema.base.utils.LoggingUtil;
+
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class BottomSheetListDialog extends BottomSheetAbstractDialog {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("BottomSheetListDialog");
+
     public static BottomSheetListDialog newInstance(@StringRes int title, ArrayList<BottomSheetItem> items, int selected) {
         BottomSheetListDialog dialog = new BottomSheetListDialog();
         Bundle args = new Bundle();
@@ -53,4 +60,10 @@ public class BottomSheetListDialog extends BottomSheetAbstractDialog {
         dialog.setArguments(args);
         return dialog;
     }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
+    }
 }

+ 8 - 0
app/src/main/java/ch/threema/app/dialogs/CallbackTextEntryDialog.kt

@@ -27,9 +27,17 @@ import android.view.View
 import android.widget.EditText
 import ch.threema.app.R
 import ch.threema.app.utils.EditTextUtil
+import ch.threema.app.utils.logScreenVisibility
+import ch.threema.base.utils.LoggingUtil
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 
+private val logger = LoggingUtil.getThreemaLogger("CallbackTextEntryDialog")
+
 class CallbackTextEntryDialog : ThreemaDialogFragment() {
+    init {
+        logScreenVisibility(logger)
+    }
+
     private var title: String? = null
     private var initialText: String? = null
     private var callback: OnButtonClickedCallback? = null

+ 7 - 0
app/src/main/java/ch/threema/app/dialogs/CancelableGenericProgressDialog.java

@@ -29,13 +29,19 @@ import android.widget.TextView;
 
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 
+import org.slf4j.Logger;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.StringRes;
 import androidx.appcompat.app.AlertDialog;
 import androidx.appcompat.app.AppCompatDialog;
 import ch.threema.app.R;
+import ch.threema.base.utils.LoggingUtil;
+
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class CancelableGenericProgressDialog extends ThreemaDialogFragment {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("CancelableGenericProgressDialog");
     private AlertDialog alertDialog;
     private Activity activity;
     private CancelableGenericProgressDialog.ProgressDialogClickListener callback;
@@ -58,6 +64,7 @@ public class CancelableGenericProgressDialog extends ThreemaDialogFragment {
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         if (callback == null) {
             try {

+ 7 - 0
app/src/main/java/ch/threema/app/dialogs/CancelableHorizontalProgressDialog.java

@@ -34,6 +34,8 @@ import android.widget.TextView;
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.android.material.progressindicator.LinearProgressIndicator;
 
+import org.slf4j.Logger;
+
 import java.text.NumberFormat;
 
 import androidx.annotation.NonNull;
@@ -42,8 +44,12 @@ import androidx.annotation.UiThread;
 import androidx.appcompat.app.AlertDialog;
 import androidx.fragment.app.FragmentManager;
 import ch.threema.app.R;
+import ch.threema.base.utils.LoggingUtil;
+
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class CancelableHorizontalProgressDialog extends ThreemaDialogFragment {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("CancelableHorizontalProgressDialog");
     private ProgressDialogClickListener callback;
     private Activity activity;
     private DialogInterface.OnClickListener listener;
@@ -112,6 +118,7 @@ public class CancelableHorizontalProgressDialog extends ThreemaDialogFragment {
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         this.mProgressPercentFormat = NumberFormat.getPercentInstance();
         this.mProgressPercentFormat.setMaximumFractionDigits(0);

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

@@ -54,6 +54,7 @@ import ch.threema.base.utils.LoggingUtil;
 import ch.threema.data.models.ContactModelData;
 import ch.threema.localcrypto.MasterKeyLockedException;
 
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 import static ch.threema.domain.models.ContactKt.CONTACT_NAME_MAX_LENGTH_BYTES;
 
 public class ContactEditDialog extends ThreemaDialogFragment implements AvatarEditView.AvatarEditListener {
@@ -178,6 +179,7 @@ public class ContactEditDialog extends ThreemaDialogFragment implements AvatarEd
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         try {
             callbackRef = new WeakReference<>((ContactEditDialogClickListener) getTargetFragment());

+ 7 - 6
app/src/main/java/ch/threema/app/dialogs/ExpandableTextEntryDialog.java

@@ -38,22 +38,22 @@ import android.widget.TextView;
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.android.material.textfield.TextInputLayout;
 
+import org.slf4j.Logger;
+
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.appcompat.app.AlertDialog;
 import androidx.appcompat.app.AppCompatDialog;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
-import ch.threema.app.services.ContactService;
-import ch.threema.app.services.GroupService;
-import ch.threema.app.services.PreferenceService;
-import ch.threema.app.services.UserService;
 import ch.threema.app.ui.ComposeEditText;
 import ch.threema.app.utils.AnimationUtil;
 import ch.threema.app.utils.TestUtil;
-import ch.threema.data.models.GroupModel;
+import ch.threema.base.utils.LoggingUtil;
+
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class ExpandableTextEntryDialog extends ThreemaDialogFragment {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("ExpandableTextEntryDialog");
     private ExpandableTextEntryDialogClickListener callback;
     private Activity activity;
     private ComposeEditText captionEditText;
@@ -111,6 +111,7 @@ public class ExpandableTextEntryDialog extends ThreemaDialogFragment {
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         if (callback == null) {
             try {

+ 12 - 0
app/src/main/java/ch/threema/app/dialogs/FormatTextEntryDialog.java

@@ -32,6 +32,8 @@ import android.widget.Button;
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.android.material.textfield.TextInputLayout;
 
+import org.slf4j.Logger;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.StringRes;
 import androidx.appcompat.app.AlertDialog;
@@ -40,8 +42,12 @@ import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.emojis.EmojiEditText;
 import ch.threema.app.utils.DialogUtil;
+import ch.threema.base.utils.LoggingUtil;
+
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class FormatTextEntryDialog extends ThreemaDialogFragment {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("FormatTextEntryDialog");
 
     public static final String ARG_TITLE = "title";
     public static final String ARG_MESSAGE = "message";
@@ -79,6 +85,12 @@ public class FormatTextEntryDialog extends ThreemaDialogFragment {
         void onNo();
     }
 
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
+    }
+
     @NonNull
     @Override
     public AppCompatDialog onCreateDialog(Bundle savedInstanceState) {

+ 8 - 0
app/src/main/java/ch/threema/app/dialogs/GenericAlertDialog.java

@@ -39,9 +39,16 @@ import androidx.fragment.app.Fragment;
 
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 
+import org.slf4j.Logger;
+
 import ch.threema.app.utils.TestUtil;
+import ch.threema.base.utils.LoggingUtil;
+
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class GenericAlertDialog extends ThreemaDialogFragment {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("GenericAlertDialog");
+
     private DialogClickListener callback;
     private Activity activity;
     private AlertDialog alertDialog;
@@ -186,6 +193,7 @@ public class GenericAlertDialog extends ThreemaDialogFragment {
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
 
         if (callback == null) {
             try {

+ 13 - 0
app/src/main/java/ch/threema/app/dialogs/GenericProgressDialog.java

@@ -28,6 +28,8 @@ import android.widget.TextView;
 
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 
+import org.slf4j.Logger;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
@@ -36,8 +38,13 @@ import androidx.appcompat.app.AlertDialog;
 import androidx.appcompat.app.AppCompatDialog;
 import androidx.fragment.app.FragmentManager;
 import ch.threema.app.R;
+import ch.threema.base.utils.LoggingUtil;
+
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 public class GenericProgressDialog extends ThreemaDialogFragment {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("GenericProgressDialog");
+
     private AlertDialog alertDialog;
     private Activity activity;
     private TextView messageTextView;
@@ -62,6 +69,12 @@ public class GenericProgressDialog extends ThreemaDialogFragment {
         return dialog;
     }
 
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
+    }
+
     @Override
     public void onAttach(@NonNull Activity activity) {
         super.onAttach(activity);

+ 12 - 0
app/src/main/java/ch/threema/app/dialogs/GroupDescEditDialog.java

@@ -27,15 +27,22 @@ import android.widget.EditText;
 
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 
+import org.slf4j.Logger;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.StringRes;
 import androidx.appcompat.app.AppCompatDialog;
 import ch.threema.app.R;
 import ch.threema.app.utils.EditTextUtil;
 import ch.threema.app.utils.TestUtil;
+import ch.threema.base.utils.LoggingUtil;
+
+import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
 
 
 public class GroupDescEditDialog extends ThreemaDialogFragment {
+    private static final Logger logger = LoggingUtil.getThreemaLogger("GroupDescEditDialog");
+
 
     private static final String ARG_TITLE = "title";
     private static final String ARG_GROUP_DESC = "groupDesc";
@@ -69,6 +76,11 @@ public class GroupDescEditDialog extends ThreemaDialogFragment {
         this.callback = callback;
     }
 
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        logScreenVisibility(this, logger);
+    }
 
     @NonNull
     @Override

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません