From a398f3b12c5d739cbff6acd4731c87e39268001d Mon Sep 17 00:00:00 2001
From: Koen <koen@pop-os.localdomain>
Date: Thu, 3 Aug 2023 17:13:25 +0200
Subject: [PATCH] Implemented Polycentric ImageBundle.

---
 .idea/androidTestResultsUserPreferences.xml   |   4 +
 .idea/misc.xml                                |   2 +-
 .../polycentric/core/ProcessHandleTests.kt    |  85 ++++++++++++++++--
 .../java/com/futo/polycentric/core/Models.kt  |  15 +++-
 .../futo/polycentric/core/ProcessHandle.kt    |  81 +++++++++--------
 .../com/futo/polycentric/core/SystemState.kt  |   7 +-
 .../core/serializers/ImageBundleSerializer.kt |  23 +++++
 .../futo/polycentric/protos/protocol.proto    |  16 +++-
 app/src/main/res/drawable/avatar.jpg          | Bin 8021 -> 0 bytes
 .../res/drawable/{banner.jpg => image.jpg}    | Bin
 10 files changed, 182 insertions(+), 51 deletions(-)
 create mode 100644 app/src/main/java/com/futo/polycentric/core/serializers/ImageBundleSerializer.kt
 delete mode 100644 app/src/main/res/drawable/avatar.jpg
 rename app/src/main/res/drawable/{banner.jpg => image.jpg} (100%)

diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml
index b624309..52521d3 100644
--- a/.idea/androidTestResultsUserPreferences.xml
+++ b/.idea/androidTestResultsUserPreferences.xml
@@ -122,8 +122,11 @@
             <AndroidTestResultsTableState>
               <option name="preferredColumnWidths">
                 <map>
+                  <entry key="29211JEGR13699" value="120" />
                   <entry key="Duration" value="90" />
+                  <entry key="Google&#10; Pixel 6a" value="120" />
                   <entry key="Pixel_3a_API_33_x86_64" value="120" />
+                  <entry key="Pixel_C_API_33" value="120" />
                   <entry key="Tests" value="360" />
                 </map>
               </option>
@@ -195,6 +198,7 @@
                   <entry key="Duration" value="90" />
                   <entry key="Google&#10; Pixel 6a" value="120" />
                   <entry key="Pixel_3a_API_33_x86_64" value="120" />
+                  <entry key="Pixel_C_API_33" value="120" />
                   <entry key="R5CNC07TZCY" value="120" />
                   <entry key="Tests" value="360" />
                   <entry key="samsung&#10; SM-G998B" value="120" />
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 8978d23..773fe0f 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,6 +1,6 @@
 <project version="4">
   <component name="ExternalStorageConfigurationManager" enabled="true" />
-  <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
     <output url="file://$PROJECT_DIR$/build/classes" />
   </component>
   <component name="ProjectType">
diff --git a/app/src/androidTest/java/com/futo/polycentric/core/ProcessHandleTests.kt b/app/src/androidTest/java/com/futo/polycentric/core/ProcessHandleTests.kt
index fa3d036..9f2c1a7 100644
--- a/app/src/androidTest/java/com/futo/polycentric/core/ProcessHandleTests.kt
+++ b/app/src/androidTest/java/com/futo/polycentric/core/ProcessHandleTests.kt
@@ -2,16 +2,25 @@ package com.futo.polycentric.core
 
 import android.content.Context
 import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
 import android.util.Log
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 import com.google.protobuf.ByteString
+import com.goterl.lazysodium.interfaces.Sign
 import org.junit.Assert.*
 import org.junit.Test
 import org.junit.runner.RunWith
 import userpackage.Protocol
 import userpackage.Protocol.QueryReferencesRequestCountReferences
 import java.io.ByteArrayOutputStream
+import java.io.DataInputStream
+
+
+
 
 @RunWith(AndroidJUnit4::class)
 class ProcessHandleTests {
@@ -86,16 +95,78 @@ class ProcessHandleTests {
         Store.initializeMemoryStore()
 
         val processHandle = ProcessHandle.create()
+        processHandle.addServer(TestConstants.SERVER)
 
-        val fakeImage = byteArrayOf(1, 2, 3, 4)
-        val imagePointer = processHandle.publishBlob("image/png", fakeImage)
-        processHandle.setAvatar(imagePointer)
+        //Load image
+        val imageBytes: ByteArray
+        run {
+            val context = getInstrumentation().targetContext
+            val imageId = context.resources.getIdentifier("image", "drawable", context.packageName)
+            val bitmap = BitmapFactory.decodeResource(context.resources, imageId)
+            val stream = ByteArrayOutputStream()
+            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)
+            imageBytes = stream.toByteArray()
+            stream.close()
+        }
 
-        val serverState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(processHandle.system))
-        val loadedAvatar = processHandle.loadBlob(serverState.avatar!!)!!
+        //Set avatar
+        val eventLogicalClock: Long
+        run {
+            eventLogicalClock = processHandle.publishAvatar(listOf(
+                ImageData("image/jpeg", 2560, 424, imageBytes)
+            )).logicalClock
+        }
 
-        assertEquals("image/png", loadedAvatar.mime)
-        assertArrayEquals(fakeImage, loadedAvatar.content)
+        processHandle.fullyBackfillServers()
+
+        //Read avatar and compare
+        run {
+            val avatarProcessRangesToGet = Protocol.RangesForProcess.newBuilder()
+                .addRanges(Protocol.Range.newBuilder().setLow(eventLogicalClock).setHigh(eventLogicalClock).build())
+                .setProcess(processHandle.processSecret.process.toProto())
+                .build()
+            val avatarEvents = ApiMethods.getEvents(TestConstants.SERVER, processHandle.system.toProto(), Protocol.RangesForSystem.newBuilder()
+                .addRangesForProcesses(avatarProcessRangesToGet).build())
+
+            val avatarSignedEvent = avatarEvents.eventsList.first()
+            val avatarEvent = SignedEvent.fromProto(avatarSignedEvent).event
+            assertEquals(ContentType.AVATAR.value, avatarEvent.contentType)
+            assertNotNull(avatarEvent.lwwElement)
+
+            val imageBundle = Protocol.ImageBundle.parseFrom(avatarEvent.lwwElement!!.value)
+            assertEquals(1, imageBundle.imageManifestsList.size)
+
+            val imageManifest = imageBundle.imageManifestsList.first()
+            assertEquals("image/jpeg", imageManifest.mime)
+            assertEquals(2560, imageManifest.width)
+            assertEquals(424, imageManifest.height)
+            assertEquals(imageBytes.size.toLong(), imageManifest.byteCount)
+            assertEquals(processHandle.processSecret.process.toProto(), imageManifest.process)
+
+            val blobProcessRangesToGet = Protocol.RangesForProcess.newBuilder()
+                .addAllRanges(imageManifest.sectionsList)
+                .setProcess(processHandle.processSecret.process.toProto())
+                .build()
+            val blobEvents = ApiMethods.getEvents(TestConstants.SERVER, processHandle.system.toProto(), Protocol.RangesForSystem.newBuilder()
+                .addRangesForProcesses(blobProcessRangesToGet).build()).eventsList.map { SignedEvent.fromProto(it) }
+
+            val totalArray = ByteArray(imageManifest.byteCount.toInt())
+            var offset = 0
+
+            for (section in imageManifest.sectionsList) {
+                val sectionEvents = blobEvents.filter { it.event.logicalClock >= section.low && it.event.logicalClock <= section.high }.sortedBy { it.event.logicalClock }
+                sectionEvents.forEach {
+                    assertEquals(ContentType.BLOB_SECTION.value, it.event.contentType)
+                    val blobSection = Protocol.BlobSection.parseFrom(it.event.content)
+                    val size = blobSection.content.size()
+                    blobSection.content.copyTo(totalArray, offset)
+                    offset += size
+                }
+            }
+
+            assertEquals(imageManifest.byteCount.toInt(), offset)
+            assertArrayEquals(imageBytes, totalArray)
+        }
     }
 
     @Test
diff --git a/app/src/main/java/com/futo/polycentric/core/Models.kt b/app/src/main/java/com/futo/polycentric/core/Models.kt
index 58c96ea..90aec85 100644
--- a/app/src/main/java/com/futo/polycentric/core/Models.kt
+++ b/app/src/main/java/com/futo/polycentric/core/Models.kt
@@ -14,7 +14,6 @@ enum class ContentType(val value: Long) {
     FOLLOW(4),
     USERNAME(5),
     DESCRIPTION(6),
-    BLOB_META(7),
     BLOB_SECTION(8),
     AVATAR(9),
     SERVER(10),
@@ -162,6 +161,20 @@ data class Pointer(
     }
 }
 
+data class ImageData(val mimeType: String, val width: Int, val height: Int, val data: ByteArray) {
+    override fun equals(other: Any?): Boolean {
+        if (other is ImageData) {
+            return mimeType == other.mimeType && width == other.width && height == other.height && data.contentEquals(other.data)
+        }
+
+        return false
+    }
+
+    override fun hashCode(): Int {
+        return combineHashCodes(listOf(width, height, mimeType.hashCode(), data.contentHashCode()))
+    }
+}
+
 data class Opinion(val data: ByteArray) {
     companion object {
         fun makeOpinion(x: Int): Opinion {
diff --git a/app/src/main/java/com/futo/polycentric/core/ProcessHandle.kt b/app/src/main/java/com/futo/polycentric/core/ProcessHandle.kt
index 1d6fb69..f235a58 100644
--- a/app/src/main/java/com/futo/polycentric/core/ProcessHandle.kt
+++ b/app/src/main/java/com/futo/polycentric/core/ProcessHandle.kt
@@ -1,7 +1,10 @@
 package com.futo.polycentric.core
 
+import android.graphics.Bitmap
+import android.util.Log
 import com.google.protobuf.ByteString
 import userpackage.Protocol
+import java.lang.Integer.min
 import java.util.*
 
 class ProcessHandle constructor(
@@ -81,10 +84,10 @@ class ProcessHandle constructor(
         )
     }
 
-    fun setAvatar(avatar: Pointer): Pointer {
+    fun setAvatar(avatar: Protocol.ImageBundle): Pointer {
         return setCRDTItem(
             ContentType.AVATAR.value,
-            avatar.toProto().toByteArray(),
+            avatar.toByteArray(),
         )
     }
 
@@ -158,49 +161,53 @@ class ProcessHandle constructor(
         )
     }
 
-    fun publishBlob(mime: String, content: ByteArray): Pointer {
-        val meta = publish(
-            ContentType.BLOB_META.value,
-            Protocol.BlobMeta.newBuilder()
-                .setSectionCount(1L)
-                .setMime(mime)
-                .build().toByteArray(),
-            null,
-            null,
-            mutableListOf()
-        )
+    fun publishBlob(content: ByteArray): List<Range> {
+        val maxBytes = 1024 * 512
+        var i = 0
+        val ranges = mutableListOf<Range>()
 
-        publish(
-            ContentType.BLOB_SECTION.value,
-            Protocol.BlobSection.newBuilder()
-                .setMetaPointer(meta.logicalClock)
-                .setContent(ByteString.copyFrom(content))
-                .build().toByteArray(),
-            null,
-            null,
-            mutableListOf()
-        )
+        while (true) {
+            if (i >= content.size - 1) {
+                break;
+            }
 
-        return meta
-    }
+            val end = min(i + maxBytes, content.size - 1)
+            val bytesToUpload = content.sliceArray(IntRange(i, end))
+            val pointer = publish(
+                ContentType.BLOB_SECTION.value,
+                Protocol.BlobSection.newBuilder()
+                    .setContent(ByteString.copyFrom(bytesToUpload))
+                    .build().toByteArray(),
+                null,
+                null,
+                mutableListOf()
+            )
 
-    fun loadBlob(pointer: Pointer): Blob? {
-        val signedEvent: SignedEvent = Store.instance.getSignedEvent(pointer) ?: return null
-        val event = signedEvent.event
-        if (event.contentType != ContentType.BLOB_META.value) {
-            return null
+            Ranges.insert(ranges, pointer.logicalClock)
+            i = end + 1
         }
 
-        val meta = Protocol.BlobMeta.parseFrom(event.content)
+        return ranges
+    }
 
-        val nextSignedEvent: SignedEvent = Store.instance.getSignedEvent(pointer.system, pointer.process, pointer.logicalClock + 1L) ?: return null
-        val nextEvent = nextSignedEvent.event
-        if (nextEvent.contentType != ContentType.BLOB_SECTION.value) {
-            return null
+    fun publishAvatar(images: List<ImageData>): Pointer {
+        val imageBundleBuilder = Protocol.ImageBundle.newBuilder()
+
+        for (image in images) {
+            val imageRanges = publishBlob(image.data)
+            val imageManifest = Protocol.ImageManifest.newBuilder()
+                .setMime(image.mimeType)
+                .setWidth(image.width.toLong())
+                .setHeight(image.height.toLong())
+                .setByteCount(image.data.size.toLong())
+                .setProcess(processSecret.process.toProto())
+                .addAllSections(imageRanges.map { it.toProto() })
+                .build()
+
+            imageBundleBuilder.addImageManifests(imageManifest)
         }
 
-        val section = Protocol.BlobSection.parseFrom(nextEvent.content)
-        return Blob(meta.mime, section.content.toByteArray())
+        return setAvatar(imageBundleBuilder.build())
     }
 
     private fun publish(contentType: Long, content: ByteArray, lwwElementSet: LWWElementSet?, lwwElement: LWWElement?, references: MutableList<Protocol.Reference>): Pointer {
diff --git a/app/src/main/java/com/futo/polycentric/core/SystemState.kt b/app/src/main/java/com/futo/polycentric/core/SystemState.kt
index 129f1d3..68d02c2 100644
--- a/app/src/main/java/com/futo/polycentric/core/SystemState.kt
+++ b/app/src/main/java/com/futo/polycentric/core/SystemState.kt
@@ -1,5 +1,6 @@
 package com.futo.polycentric.core
 
+import com.futo.polycentric.core.serializers.ImageBundleSerializer
 import kotlinx.serialization.Serializable
 import userpackage.Protocol
 
@@ -10,7 +11,7 @@ class SystemState(
     val username: String,
     val description: String,
     val store: String,
-    val avatar: Pointer?,
+    @Serializable(with = ImageBundleSerializer::class) val avatar: Protocol.ImageBundle?,
     val banner: Pointer?
 ) {
     override fun toString(): String {
@@ -29,7 +30,7 @@ class SystemState(
             var username = ""
             var description = ""
             var store = ""
-            var avatar: Pointer? = null
+            var avatar: Protocol.ImageBundle? = null
             var banner: Pointer? = null
             proto.crdtItems.forEach { item ->
                 when (item.contentType) {
@@ -43,7 +44,7 @@ class SystemState(
                         store = item.value.decodeToString()
                     }
                     ContentType.AVATAR.value -> {
-                        avatar = Pointer.fromProto(Protocol.Pointer.parseFrom(item.value))
+                        avatar = Protocol.ImageBundle.parseFrom(item.value)
                     }
                     ContentType.BANNER.value -> {
                         banner = Pointer.fromProto(Protocol.Pointer.parseFrom(item.value))
diff --git a/app/src/main/java/com/futo/polycentric/core/serializers/ImageBundleSerializer.kt b/app/src/main/java/com/futo/polycentric/core/serializers/ImageBundleSerializer.kt
new file mode 100644
index 0000000..6fcdc7c
--- /dev/null
+++ b/app/src/main/java/com/futo/polycentric/core/serializers/ImageBundleSerializer.kt
@@ -0,0 +1,23 @@
+package com.futo.polycentric.core.serializers
+
+import com.futo.polycentric.core.base64UrlToByteArray
+import com.futo.polycentric.core.toBase64Url
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import userpackage.Protocol
+
+class ImageBundleSerializer : KSerializer<Protocol.ImageBundle> {
+    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ImageBundle", PrimitiveKind.STRING)
+
+    override fun serialize(encoder: Encoder, value: Protocol.ImageBundle) {
+        encoder.encodeString(value.toByteArray().toBase64Url())
+    }
+    override fun deserialize(decoder: Decoder): Protocol.ImageBundle {
+        val value = decoder.decodeString();
+        return Protocol.ImageBundle.parseFrom(value.base64UrlToByteArray());
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/futo/polycentric/protos/protocol.proto b/app/src/main/proto/com/futo/polycentric/protos/protocol.proto
index b858ad6..214825d 100644
--- a/app/src/main/proto/com/futo/polycentric/protos/protocol.proto
+++ b/app/src/main/proto/com/futo/polycentric/protos/protocol.proto
@@ -54,8 +54,20 @@ message BlobMeta {
 }
 
 message BlobSection {
-    uint64 meta_pointer = 1;
-    bytes content = 2;
+    bytes content = 1;
+}
+
+message ImageManifest {
+             string  mime       = 1;
+             uint64  width      = 2;
+             uint64  height     = 3;
+             uint64  byte_count = 4;
+             Process process    = 5; 
+    repeated Range   sections   = 6;
+}
+
+message ImageBundle {
+    repeated ImageManifest image_manifests = 1;
 }
 
 message Event {
diff --git a/app/src/main/res/drawable/avatar.jpg b/app/src/main/res/drawable/avatar.jpg
deleted file mode 100644
index 6586937a09d2369a7b12676c118e232cfcfc8e4c..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 8021
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
TgvPmLR$FzMB;8@=e&zoGr)YyC

diff --git a/app/src/main/res/drawable/banner.jpg b/app/src/main/res/drawable/image.jpg
similarity index 100%
rename from app/src/main/res/drawable/banner.jpg
rename to app/src/main/res/drawable/image.jpg
-- 
GitLab