Repository: thunderbird/thunderbird-android (language: kotlin)

Issue:
Rewrite AutocryptHeader and AutocryptGossipHeader in Kotlin instead of Java
We should migrate the `AutocryptHeader`  and `AutocryptGossipHeader` to Kotlin to align with the ongoing modernization efforts and Kotlin-first development approach across the codebase.


Discussion hints:
nan

Pull request: Rewrite AutocryptHeader and AutocryptGossipHeader in Kotlin instead of Java
 - Fixes #9758 
 - **Summary** : This PR refactors the existing AutocryptHeader and AutocryptGossipHeader classes from Java into idiomatic Kotlin. The conversion simplifies code, enhances null safety, and leverages Kotlin’s modern language features.
 - **Implementation Highlights**:
   - `AutocryptHeader.kt`: 
       - Rewritten as a Kotlin class with primary constructor parameters:
          - parameters: Map<String, String>
          - addr: String
          - keyData: ByteArray
          - isPreferEncryptMutual: Boolean
     -  `toRawHeaderString()` method builds the header string with support for folding base64 key data.
     - Overridden `equals()` and `hashCode()` methods based on Kotlin's conventions.
     - Companion object defines constants such as **AUTOCRYPT_HEADER**, **AUTOCRYPT_PARAM_ADDR**, and a helper `createFoldedBase64KeyData()` method for formatting
   - `AutocryptGossipHeader.kt` :
     - Defined as an internal data class:
       - Fields: addr: String, keyData: ByteArray
       - `toRawHeaderString()` method similar to AutocryptHeader
       - equals() and hashCode() override using contentEquals() and contentHashCode() for byte arrays
       - Companion object includes constants: **AUTOCRYPT_GOSSIP_HEADER**, **AUTOCRYPT_PARAM_ADDR**, **AUTOCRYPT_PARAM_KEY_DATA**
    - **Updated references in codebase**: 
      - Minor adjustments in `AutocryptOperations.java` to accommodate Kotlin properties instead of Java getters: moved from `autocryptHeader.getKeyData()` to Kotlin-style property access` autocryptHeader.keyData`, and similarly for other fields 
    - **Test updates**: Refactored `AutocryptHeaderParserTest.java` to reflect Kotlin property access instead of Java getter methods, ensuring test compatibility:
      - Replaced `autocryptHeader.getAddr()`, .`getKeyData(`), .`getParameters()` with Kotlin property access autocryptHeader.addr, .keyData, .parameters
      - Adjusted input stream handling for test resource loading (readFromResourceFile) to account for simpler method signature
     


Test command: ./gradlew :legacy:core:testDebugUnitTest --tests "com.fsck.k9.autocrypt.AutocryptHeaderParserTest"

Fix patch:
diff --git a/legacy/core/src/main/java/com/fsck/k9/autocrypt/AutocryptGossipHeader.java b/legacy/core/src/main/java/com/fsck/k9/autocrypt/AutocryptGossipHeader.java
deleted file mode 100644
index 91d9b788365..00000000000
--- a/legacy/core/src/main/java/com/fsck/k9/autocrypt/AutocryptGossipHeader.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package com.fsck.k9.autocrypt;
-
-
-import java.util.Arrays;
-
-import androidx.annotation.NonNull;
-
-
-class AutocryptGossipHeader {
-    static final String AUTOCRYPT_GOSSIP_HEADER = "Autocrypt-Gossip";
-
-    private static final String AUTOCRYPT_PARAM_ADDR = "addr";
-    private static final String AUTOCRYPT_PARAM_KEY_DATA = "keydata";
-
-
-    @NonNull
-    final byte[] keyData;
-    @NonNull
-    final String addr;
-
-    AutocryptGossipHeader(@NonNull String addr, @NonNull byte[] keyData) {
-        this.addr = addr;
-        this.keyData = keyData;
-    }
-
-    String toRawHeaderString() {
-        StringBuilder builder = new StringBuilder();
-
-        builder.append(AutocryptGossipHeader.AUTOCRYPT_GOSSIP_HEADER).append(": ");
-        builder.append(AutocryptGossipHeader.AUTOCRYPT_PARAM_ADDR).append('=').append(addr).append("; ");
-        builder.append(AutocryptGossipHeader.AUTOCRYPT_PARAM_KEY_DATA).append('=');
-        builder.append(AutocryptHeader.createFoldedBase64KeyData(keyData));
-
-        return builder.toString();
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (o == null || getClass() != o.getClass()) {
-            return false;
-        }
-
-        AutocryptGossipHeader that = (AutocryptGossipHeader) o;
-
-        if (!Arrays.equals(keyData, that.keyData)) {
-            return false;
-        }
-        return addr.equals(that.addr);
-    }
-
-    @Override
-    public int hashCode() {
-        int result = Arrays.hashCode(keyData);
-        result = 31 * result + addr.hashCode();
-        return result;
-    }
-}
diff --git a/legacy/core/src/main/java/com/fsck/k9/autocrypt/AutocryptGossipHeader.kt b/legacy/core/src/main/java/com/fsck/k9/autocrypt/AutocryptGossipHeader.kt
new file mode 100644
index 00000000000..9aa26b41d4d
--- /dev/null
+++ b/legacy/core/src/main/java/com/fsck/k9/autocrypt/AutocryptGossipHeader.kt
@@ -0,0 +1,25 @@
+package com.fsck.k9.autocrypt
+
+internal data class AutocryptGossipHeader(@JvmField val addr: String, @JvmField val keyData: ByteArray) {
+    fun toRawHeaderString() = buildString {
+        append(AUTOCRYPT_GOSSIP_HEADER).append(": ")
+        append(AUTOCRYPT_PARAM_ADDR).append('=').append(addr).append("; ")
+        append(AUTOCRYPT_PARAM_KEY_DATA).append('=')
+        append(AutocryptHeader.createFoldedBase64KeyData(keyData))
+    }
+
+    override fun equals(other: Any?): Boolean =
+        other is AutocryptGossipHeader &&
+            addr == other.addr &&
+            keyData.contentEquals(other.keyData)
+
+    override fun hashCode(): Int =
+        31 * keyData.contentHashCode() + addr.hashCode()
+
+    companion object {
+        const val AUTOCRYPT_GOSSIP_HEADER: String = "Autocrypt-Gossip"
+
+        private const val AUTOCRYPT_PARAM_ADDR = "addr"
+        private const val AUTOCRYPT_PARAM_KEY_DATA = "keydata"
+    }
+}
diff --git a/legacy/core/src/main/java/com/fsck/k9/autocrypt/AutocryptHeader.java b/legacy/core/src/main/java/com/fsck/k9/autocrypt/AutocryptHeader.java
deleted file mode 100644
index d5d66710314..00000000000
--- a/legacy/core/src/main/java/com/fsck/k9/autocrypt/AutocryptHeader.java
+++ /dev/null
@@ -1,102 +0,0 @@
-package com.fsck.k9.autocrypt;
-
-
-import java.util.Arrays;
-import java.util.Map;
-
-import androidx.annotation.NonNull;
-
-import okio.ByteString;
-
-
-class AutocryptHeader {
-    static final String AUTOCRYPT_HEADER = "Autocrypt";
-
-    static final String AUTOCRYPT_PARAM_ADDR = "addr";
-    static final String AUTOCRYPT_PARAM_KEY_DATA = "keydata";
-
-    static final String AUTOCRYPT_PARAM_TYPE = "type";
-    static final String AUTOCRYPT_TYPE_1 = "1";
-
-    static final String AUTOCRYPT_PARAM_PREFER_ENCRYPT = "prefer-encrypt";
-    static final String AUTOCRYPT_PREFER_ENCRYPT_MUTUAL = "mutual";
-
-    private static final int HEADER_LINE_LENGTH = 76;
-
-
-    @NonNull
-    final byte[] keyData;
-    @NonNull
-    final String addr;
-    @NonNull
-    final Map<String,String> parameters;
-    final boolean isPreferEncryptMutual;
-
-    AutocryptHeader(@NonNull Map<String, String> parameters, @NonNull String addr,
-            @NonNull byte[] keyData, boolean isPreferEncryptMutual) {
-        this.parameters = parameters;
-        this.addr = addr;
-        this.keyData = keyData;
-        this.isPreferEncryptMutual = isPreferEncryptMutual;
-    }
-
-    String toRawHeaderString() {
-        // TODO we don't properly fold lines here. if we want to support parameters, we need to do that somehow
-        if (!parameters.isEmpty()) {
-            throw new UnsupportedOperationException("arbitrary parameters not supported");
-        }
-
-        StringBuilder builder = new StringBuilder();
-        builder.append(AutocryptHeader.AUTOCRYPT_HEADER).append(": ");
-        builder.append(AutocryptHeader.AUTOCRYPT_PARAM_ADDR).append('=').append(addr).append("; ");
-        if (isPreferEncryptMutual) {
-            builder.append(AutocryptHeader.AUTOCRYPT_PARAM_PREFER_ENCRYPT)
-                    .append('=').append(AutocryptHeader.AUTOCRYPT_PREFER_ENCRYPT_MUTUAL).append("; ");
-        }
-        builder.append(AutocryptHeader.AUTOCRYPT_PARAM_KEY_DATA).append("=");
-        builder.append(createFoldedBase64KeyData(keyData));
-
-        return builder.toString();
-    }
-
-    static String createFoldedBase64KeyData(byte[] keyData) {
-        String base64KeyData = ByteString.of(keyData).base64();
-        StringBuilder result = new StringBuilder();
-
-        for (int i = 0, base64Length = base64KeyData.length(); i < base64Length; i += HEADER_LINE_LENGTH) {
-            if (i + HEADER_LINE_LENGTH <= base64Length) {
-                result.append("\r\n ");
-                result.append(base64KeyData, i, i + HEADER_LINE_LENGTH);
-            } else {
-                result.append("\r\n ");
-                result.append(base64KeyData, i, base64Length);
-            }
-        }
-
-        return result.toString();
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (o == null || getClass() != o.getClass()) {
-            return false;
-        }
-
-        AutocryptHeader that = (AutocryptHeader) o;
-
-        return isPreferEncryptMutual == that.isPreferEncryptMutual && Arrays.equals(keyData, that.keyData)
-                && addr.equals(that.addr) && parameters.equals(that.parameters);
-    }
-
-    @Override
-    public int hashCode() {
-        int result = Arrays.hashCode(keyData);
-        result = 31 * result + addr.hashCode();
-        result = 31 * result + parameters.hashCode();
-        result = 31 * result + (isPreferEncryptMutual ? 1 : 0);
-        return result;
-    }
-}
diff --git a/legacy/core/src/main/java/com/fsck/k9/autocrypt/AutocryptHeader.kt b/legacy/core/src/main/java/com/fsck/k9/autocrypt/AutocryptHeader.kt
new file mode 100644
index 00000000000..2d61be5e4a4
--- /dev/null
+++ b/legacy/core/src/main/java/com/fsck/k9/autocrypt/AutocryptHeader.kt
@@ -0,0 +1,78 @@
+package com.fsck.k9.autocrypt
+
+import okio.ByteString
+
+data class AutocryptHeader(
+    val parameters: Map<String, String>,
+    val addr: String,
+    val keyData: ByteArray,
+    val isPreferEncryptMutual: Boolean,
+) {
+
+    fun toRawHeaderString(): String {
+        // TODO we don't properly fold lines here. if we want to support parameters, we need to do that somehow
+        if (parameters.isNotEmpty()) {
+            error("arbitrary parameters not supported")
+        }
+
+        return buildString {
+            append(AUTOCRYPT_HEADER).append(": ")
+            append(AUTOCRYPT_PARAM_ADDR).append('=').append(addr).append("; ")
+            if (isPreferEncryptMutual) {
+                append(AUTOCRYPT_PARAM_PREFER_ENCRYPT)
+                    .append('=').append(AUTOCRYPT_PREFER_ENCRYPT_MUTUAL).append("; ")
+            }
+            append(AUTOCRYPT_PARAM_KEY_DATA).append("=")
+            append(createFoldedBase64KeyData(keyData))
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is AutocryptHeader) return false
+
+        return isPreferEncryptMutual == other.isPreferEncryptMutual &&
+            keyData.contentEquals(other.keyData) &&
+            addr == other.addr &&
+            parameters == other.parameters
+    }
+
+    override fun hashCode(): Int {
+        var result = keyData.contentHashCode()
+        result = 31 * result + addr.hashCode()
+        result = 31 * result + parameters.hashCode()
+        result = 31 * result + if (isPreferEncryptMutual) 1 else 0
+        return result
+    }
+
+    companion object {
+        const val AUTOCRYPT_HEADER = "Autocrypt"
+
+        const val AUTOCRYPT_PARAM_ADDR = "addr"
+        const val AUTOCRYPT_PARAM_KEY_DATA = "keydata"
+
+        const val AUTOCRYPT_PARAM_TYPE = "type"
+        const val AUTOCRYPT_TYPE_1 = "1"
+
+        const val AUTOCRYPT_PARAM_PREFER_ENCRYPT = "prefer-encrypt"
+        const val AUTOCRYPT_PREFER_ENCRYPT_MUTUAL = "mutual"
+
+        private const val HEADER_LINE_LENGTH = 76
+
+        fun createFoldedBase64KeyData(keyData: ByteArray): String {
+            val base64KeyData = ByteString.of(data = keyData).base64()
+            val result = StringBuilder()
+
+            for (i in base64KeyData.indices step HEADER_LINE_LENGTH) {
+                result.append("\r\n ")
+                if (i + HEADER_LINE_LENGTH <= base64KeyData.length) {
+                    result.append(base64KeyData, i, i + HEADER_LINE_LENGTH)
+                } else {
+                    result.append(base64KeyData, i, base64KeyData.length)
+                }
+            }
+
+            return result.toString()
+        }
+    }
+}
diff --git a/legacy/core/src/main/java/com/fsck/k9/autocrypt/AutocryptOperations.java b/legacy/core/src/main/java/com/fsck/k9/autocrypt/AutocryptOperations.java
index 206733fa580..c8654d8e397 100644
--- a/legacy/core/src/main/java/com/fsck/k9/autocrypt/AutocryptOperations.java
+++ b/legacy/core/src/main/java/com/fsck/k9/autocrypt/AutocryptOperations.java
@@ -44,7 +44,7 @@ public boolean addAutocryptPeerUpdateToIntentIfPresent(Message currentMessage, I
         }
 
         String messageFromAddress = currentMessage.getFrom()[0].getAddress();
-        if (!autocryptHeader.addr.equalsIgnoreCase(messageFromAddress)) {
+        if (!autocryptHeader.getAddr().equalsIgnoreCase(messageFromAddress)) {
             return false;
         }
 
@@ -53,7 +53,7 @@ public boolean addAutocryptPeerUpdateToIntentIfPresent(Message currentMessage, I
         Date effectiveDate = messageDate.before(internalDate) ? messageDate : internalDate;
 
         AutocryptPeerUpdate data = AutocryptPeerUpdate.create(
-                autocryptHeader.keyData, effectiveDate, autocryptHeader.isPreferEncryptMutual);
+            autocryptHeader.getKeyData(), effectiveDate, autocryptHeader.isPreferEncryptMutual());
         intent.putExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_ID, messageFromAddress);
         intent.putExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_UPDATE, data);
         return true;


Test patch:
diff --git a/legacy/core/src/test/java/com/fsck/k9/autocrypt/AutocryptHeaderParserTest.java b/legacy/core/src/test/java/com/fsck/k9/autocrypt/AutocryptHeaderParserTest.java
index 4aefaecfcb1..1b2d996d5ff 100644
--- a/legacy/core/src/test/java/com/fsck/k9/autocrypt/AutocryptHeaderParserTest.java
+++ b/legacy/core/src/test/java/com/fsck/k9/autocrypt/AutocryptHeaderParserTest.java
@@ -1,14 +1,15 @@
 package com.fsck.k9.autocrypt;
 
 
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 
-import net.thunderbird.core.android.testing.RobolectricTest;
-import net.thunderbird.core.common.exception.MessagingException;
 import com.fsck.k9.mail.internet.BinaryTempFileBody;
 import com.fsck.k9.mail.internet.MimeMessage;
+import net.thunderbird.core.android.testing.RobolectricTest;
+import net.thunderbird.core.common.exception.MessagingException;
+import net.thunderbird.core.logging.legacy.Log;
+import net.thunderbird.core.logging.testing.TestLogger;
 import org.junit.Before;
 import org.junit.Test;
 import org.robolectric.RuntimeEnvironment;
@@ -23,6 +24,7 @@ public class AutocryptHeaderParserTest extends RobolectricTest {
 
     @Before
     public void setUp() throws Exception {
+        Log.logger = new TestLogger();
         BinaryTempFileBody.setTempDirectory(RuntimeEnvironment.getApplication().getCacheDir());
     }
 
@@ -53,9 +55,9 @@ public void getValidAutocryptHeader__withSimpleAutocrypt() throws Exception {
         AutocryptHeader autocryptHeader = autocryptHeaderParser.getValidAutocryptHeader(message);
 
         assertNotNull(autocryptHeader);
-        assertEquals("alice@testsuite.autocrypt.org", autocryptHeader.addr);
-        assertEquals(0, autocryptHeader.parameters.size());
-        assertEquals(1225, autocryptHeader.keyData.length);
+        assertEquals("alice@testsuite.autocrypt.org", autocryptHeader.getAddr());
+        assertEquals(0, autocryptHeader.getParameters().size());
+        assertEquals(1225, autocryptHeader.getKeyData().length);
     }
 
     @Test
@@ -65,8 +67,8 @@ public void getValidAutocryptHeader__withExplicitType() throws Exception {
         AutocryptHeader autocryptHeader = autocryptHeaderParser.getValidAutocryptHeader(message);
 
         assertNotNull(autocryptHeader);
-        assertEquals("alice@testsuite.autocrypt.org", autocryptHeader.addr);
-        assertEquals(0, autocryptHeader.parameters.size());
+        assertEquals("alice@testsuite.autocrypt.org", autocryptHeader.getAddr());
+        assertEquals(0, autocryptHeader.getParameters().size());
     }
 
     @Test
@@ -94,9 +96,9 @@ public void getValidAutocryptHeader__withUnknownNonCriticalHeader() throws Excep
         AutocryptHeader autocryptHeader = autocryptHeaderParser.getValidAutocryptHeader(message);
 
         assertNotNull(autocryptHeader);
-        assertEquals("alice@testsuite.autocrypt.org", autocryptHeader.addr);
-        assertEquals(1, autocryptHeader.parameters.size());
-        assertEquals("ignore", autocryptHeader.parameters.get("_monkey"));
+        assertEquals("alice@testsuite.autocrypt.org", autocryptHeader.getAddr());
+        assertEquals(1, autocryptHeader.getParameters().size());
+        assertEquals("ignore", autocryptHeader.getParameters().get("_monkey"));
     }
 
     @Test
@@ -104,6 +106,7 @@ public void parseAutocryptHeader_toRawHeaderString() throws Exception {
         MimeMessage message = parseFromResource("autocrypt/rsa2048-simple.eml");
         AutocryptHeader autocryptHeader = autocryptHeaderParser.getValidAutocryptHeader(message);
 
+        assertNotNull(autocryptHeader);
         String headerValue = autocryptHeader.toRawHeaderString();
         headerValue = headerValue.substring("Autocrypt: ".length());
         AutocryptHeader parsedAutocryptHeader = autocryptHeaderParser.parseAutocryptHeader(headerValue);
@@ -116,7 +119,7 @@ private MimeMessage parseFromResource(String resourceName) throws IOException, M
         return MimeMessage.parseMimeMessage(inputStream, false);
     }
 
-    private InputStream readFromResourceFile(String name) throws FileNotFoundException {
+    private InputStream readFromResourceFile(String name) {
         return getClass().getResourceAsStream("/" + name);
     }
 }
diff --git a/legacy/ui/legacy/src/test/java/com/fsck/k9/autocrypt/AutocryptOperationsHelper.java b/legacy/ui/legacy/src/test/java/com/fsck/k9/autocrypt/AutocryptOperationsHelper.java
index a939ee81fb3..34a52e81799 100644
--- a/legacy/ui/legacy/src/test/java/com/fsck/k9/autocrypt/AutocryptOperationsHelper.java
+++ b/legacy/ui/legacy/src/test/java/com/fsck/k9/autocrypt/AutocryptOperationsHelper.java
@@ -16,8 +16,8 @@ public static void assertMessageHasAutocryptHeader(
         AutocryptHeader autocryptHeader = INSTANCE.getValidAutocryptHeader(message);
 
         assertNotNull(autocryptHeader);
-        assertEquals(addr, autocryptHeader.addr);
-        assertEquals(isPreferEncryptMutual, autocryptHeader.isPreferEncryptMutual);
-        assertArrayEquals(keyData, autocryptHeader.keyData);
+        assertEquals(addr, autocryptHeader.getAddr());
+        assertEquals(isPreferEncryptMutual, autocryptHeader.isPreferEncryptMutual());
+        assertArrayEquals(keyData, autocryptHeader.getKeyData());
     }
 }