Merge "[Geofence] Write/read entry value byte count and version number to/from the header block" into main
diff --git a/utils/satellite/s2storage/src/readonly/java/com/android/telephony/sats2range/read/HeaderBlock.java b/utils/satellite/s2storage/src/readonly/java/com/android/telephony/sats2range/read/HeaderBlock.java
index 9895d1a..9592e1e 100644
--- a/utils/satellite/s2storage/src/readonly/java/com/android/telephony/sats2range/read/HeaderBlock.java
+++ b/utils/satellite/s2storage/src/readonly/java/com/android/telephony/sats2range/read/HeaderBlock.java
@@ -40,10 +40,26 @@
         int suffixBitCount = blockData.getUnsignedByte(offset++);
         int suffixRecordBitCount = blockData.getUnsignedByte(offset++);
         int suffixTableBlockIdOffset = blockData.getUnsignedByte(offset++);
-        boolean isAllowedList = (blockData.getUnsignedByte(offset) == TRUE);
-        mFileFormat = new SatS2RangeFileFormat(
-                dataS2Level, prefixBitCount, suffixBitCount, suffixTableBlockIdOffset,
-                suffixRecordBitCount, isAllowedList);
+        boolean isAllowedList = (blockData.getUnsignedByte(offset++) == TRUE);
+
+
+        // Check if the block is in the original format or the enhanced format.
+        // If the offset is equal to the block data size, this block is in the original format.
+        // If the offset is less than the block data size, this block is an enhanced block, which
+        // has additional fields:
+        //  - the size of an entry value in bytes
+        //  - version number of header block
+        if (offset < blockData.getSize()) {
+            int entryValueSizeInBytes = blockData.getUnsignedByte(offset++);
+            int versionNumber = blockData.getInt(offset);
+            mFileFormat = new SatS2RangeFileFormat(
+                    dataS2Level, prefixBitCount, suffixBitCount, suffixTableBlockIdOffset,
+                    suffixRecordBitCount, isAllowedList, entryValueSizeInBytes, versionNumber);
+        } else {
+            mFileFormat = new SatS2RangeFileFormat(
+                    dataS2Level, prefixBitCount, suffixBitCount, suffixTableBlockIdOffset,
+                    suffixRecordBitCount, isAllowedList);
+        }
     }
 
     /** Creates a {@link HeaderBlock} from low-level block data from a block file. */
diff --git a/utils/satellite/s2storage/src/readonly/java/com/android/telephony/sats2range/read/SatS2RangeFileFormat.java b/utils/satellite/s2storage/src/readonly/java/com/android/telephony/sats2range/read/SatS2RangeFileFormat.java
index 39507aa..a335766 100644
--- a/utils/satellite/s2storage/src/readonly/java/com/android/telephony/sats2range/read/SatS2RangeFileFormat.java
+++ b/utils/satellite/s2storage/src/readonly/java/com/android/telephony/sats2range/read/SatS2RangeFileFormat.java
@@ -53,6 +53,10 @@
     /** The format version of the satellite S2 data file, read and written. */
     public static final int VERSION = 1;
 
+    private static final int DEFAULT_ENTRY_VALUE_SIZE_IN_BYTES = 0;
+    private static final int DEFAULT_VERSION_NUMBER = 0;
+    private static final int MAX_ENTRY_BYTE_COUNT = 4;
+
     private final int mDataS2Level;
 
     private final int mPrefixBitCount;
@@ -87,11 +91,28 @@
     private final boolean mIsAllowedList;
 
     /**
+     * Entry value size in bytes
+     */
+    private final int mEntryValueSizeInBytes;
+
+    /**
+     * Version number
+     */
+    private final int mVersionNumber;
+
+    public SatS2RangeFileFormat(int s2Level, int prefixBitCount, int suffixBitCount,
+            int suffixTableBlockIdOffset, int tableEntryBitCount, boolean isAllowedList) {
+        this(s2Level, prefixBitCount, suffixBitCount, suffixTableBlockIdOffset, tableEntryBitCount,
+                isAllowedList, DEFAULT_ENTRY_VALUE_SIZE_IN_BYTES, DEFAULT_VERSION_NUMBER);
+    }
+
+    /**
      * Creates a new file format. This constructor validates the values against various hard-coded
      * constraints and will throw an {@link IllegalArgumentException} if they are not satisfied.
      */
     public SatS2RangeFileFormat(int s2Level, int prefixBitCount, int suffixBitCount,
-            int suffixTableBlockIdOffset, int tableEntryBitCount, boolean isAllowedList) {
+            int suffixTableBlockIdOffset, int tableEntryBitCount, boolean isAllowedList,
+            int entryValueSizeInBytes, int versionNumber) {
 
         Conditions.checkArgInRange("s2Level", s2Level, 0, MAX_S2_LEVEL);
 
@@ -180,6 +201,12 @@
         mSuffixTableBlockIdOffset = suffixTableBlockIdOffset;
 
         mIsAllowedList = isAllowedList;
+
+        Conditions.checkArgInRange("entryValueSizeInBytes", entryValueSizeInBytes, 0,
+                MAX_ENTRY_BYTE_COUNT);
+        mEntryValueSizeInBytes = entryValueSizeInBytes;
+
+        mVersionNumber = versionNumber;
     }
 
     /** Returns the S2 level of all geo data stored in the file. */
@@ -345,6 +372,18 @@
                 + "}";
     }
 
+    /**
+     * Returns the length of entry value in Bytes.
+     * @return the length of entry value
+     */
+    public int getEntryValueSizeInBytes() {
+        return mEntryValueSizeInBytes;
+    }
+
+    public int getVersionNumber() {
+        return mVersionNumber;
+    }
+
     @Override
     public String toString() {
         return "SatS2RangeFileFormat{"
@@ -359,6 +398,8 @@
                 + ", mSuffixTableBlockIdOffset=" + mSuffixTableBlockIdOffset
                 + ", mUnusedCellIdBitCount=" + mUnusedCellIdBitCount
                 + ", mIsAllowedList=" + mIsAllowedList
+                + ", mEntryValueSizeInBytes=" + mEntryValueSizeInBytes
+                + ", mVersionNumber=" + mVersionNumber
                 + '}';
     }
 
@@ -381,7 +422,9 @@
                 && mTableEntryMaxRangeLengthValue == that.mTableEntryMaxRangeLengthValue
                 && mSuffixTableBlockIdOffset == that.mSuffixTableBlockIdOffset
                 && mIsAllowedList == that.mIsAllowedList
-                && mUnusedCellIdBitCount == that.mUnusedCellIdBitCount;
+                && mUnusedCellIdBitCount == that.mUnusedCellIdBitCount
+                && mEntryValueSizeInBytes == that.mEntryValueSizeInBytes
+                && mVersionNumber == that.mVersionNumber;
     }
 
     @Override
@@ -389,7 +432,7 @@
         return Objects.hash(mDataS2Level, mPrefixBitCount, mMaxPrefixValue, mSuffixBitCount,
                 mMaxSuffixValue, mTableEntryBitCount, mTableEntryRangeLengthBitCount,
                 mTableEntryMaxRangeLengthValue, mSuffixTableBlockIdOffset, mIsAllowedList,
-                mUnusedCellIdBitCount);
+                mUnusedCellIdBitCount, mEntryValueSizeInBytes, mVersionNumber);
     }
 
     private void checkS2Level(String name, long cellId) {
diff --git a/utils/satellite/s2storage/src/readonly/java/com/android/telephony/sats2range/read/SuffixTableRange.java b/utils/satellite/s2storage/src/readonly/java/com/android/telephony/sats2range/read/SuffixTableRange.java
index 88f1b2e..8c1466f 100644
--- a/utils/satellite/s2storage/src/readonly/java/com/android/telephony/sats2range/read/SuffixTableRange.java
+++ b/utils/satellite/s2storage/src/readonly/java/com/android/telephony/sats2range/read/SuffixTableRange.java
@@ -23,12 +23,12 @@
 import java.util.Objects;
 
 public final class SuffixTableRange extends S2LevelRange {
-    private static final int DEAFAULT_ENTRY_VALUE = -1;
+    private static final int DEFAULT_ENTRY_VALUE = -1;
     private final int mEntryValue;
 
     // For backward compatibility
     public SuffixTableRange(long startCellId, long endCellId) {
-        this(startCellId, endCellId, DEAFAULT_ENTRY_VALUE);
+        this(startCellId, endCellId, DEFAULT_ENTRY_VALUE);
     }
 
     public SuffixTableRange(long startCellId, long endCellId, int entryValue) {
diff --git a/utils/satellite/s2storage/src/readonly/java/com/android/telephony/sats2range/read/SuffixTableSharedData.java b/utils/satellite/s2storage/src/readonly/java/com/android/telephony/sats2range/read/SuffixTableSharedData.java
index 2221b2c..14cb92f 100644
--- a/utils/satellite/s2storage/src/readonly/java/com/android/telephony/sats2range/read/SuffixTableSharedData.java
+++ b/utils/satellite/s2storage/src/readonly/java/com/android/telephony/sats2range/read/SuffixTableSharedData.java
@@ -16,11 +16,13 @@
 
 package com.android.telephony.sats2range.read;
 
+import com.android.storage.block.read.TypedData;
 import com.android.storage.io.read.TypedInputStream;
 import com.android.storage.table.reader.Table;
 
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.util.List;
 import java.util.Objects;
 
 /**
@@ -28,14 +30,98 @@
  * entries in the table and is required when interpreting the table's block data.
  */
 public final class SuffixTableSharedData {
-
+    public static final int INVALID_ENTRY_VALUE = -1;
     private final int mTablePrefix;
+    private final int mEntryValueSizeInBytes;
+    private final int mNumberOfEntryValues;
+    private final int mHeaderByteOffsetToRead;
+    private List<Integer> mEntryValuesToWrite = List.of(); // This is used for write path
+    private final TypedData mSharedDataToRead; // This is used for read path
 
     /**
      * Creates a {@link SuffixTableSharedData}. See also {@link #fromBytes(byte[])}.
      */
     public SuffixTableSharedData(int tablePrefix) {
         mTablePrefix = tablePrefix;
+        mEntryValueSizeInBytes = 0;
+        mNumberOfEntryValues = 0;
+        mHeaderByteOffsetToRead = 0;
+        mSharedDataToRead = null;
+    }
+
+    /**
+     * This constructor is used for write path
+     */
+    public SuffixTableSharedData(int tablePrefix, List<Integer> entryValues,
+            SatS2RangeFileFormat fileFormat) {
+        mSharedDataToRead = null;
+        mTablePrefix = tablePrefix;
+        mNumberOfEntryValues = entryValues.size();
+        mEntryValuesToWrite = entryValues;
+        mEntryValueSizeInBytes = fileFormat.getEntryValueSizeInBytes();
+        mHeaderByteOffsetToRead = 0;
+    }
+
+    /**
+     * This constructor is used for read path
+     */
+    public SuffixTableSharedData(TypedData sharedDataToRead, SatS2RangeFileFormat fileFormat) {
+        mSharedDataToRead = Objects.requireNonNull(sharedDataToRead);
+        int offset = 0;
+        // extract prefix value
+        mTablePrefix = mSharedDataToRead.getInt(offset);
+        offset += Integer.BYTES;
+
+        // If the size of shared data is greater than the offset, extract the number of entry
+        // values.
+        if ((offset + Integer.BYTES) < mSharedDataToRead.getSize()) {
+            mNumberOfEntryValues = mSharedDataToRead.getInt(offset);
+            mHeaderByteOffsetToRead = offset + Integer.BYTES;
+            mEntryValueSizeInBytes = fileFormat.getEntryValueSizeInBytes();
+        } else {
+            mNumberOfEntryValues = 0;
+            mHeaderByteOffsetToRead = offset;
+            mEntryValueSizeInBytes = 0;
+        }
+    }
+
+    /**
+     * This is used for read path
+     */
+    public static SuffixTableSharedData fromTypedData(TypedData sharedData,
+            SatS2RangeFileFormat fileFormat) {
+        return new SuffixTableSharedData(sharedData, fileFormat);
+    }
+
+    /**
+     * Reads the entry value at a specific position in the byte buffer and returns it.
+     *
+     * @param entryIndex The index of entry to be read.
+     * @return entry value (integer) read from the byte buffer.
+     */
+    public int getEntryValue(int entryIndex) {
+        if (mSharedDataToRead == null || entryIndex < 0 || mNumberOfEntryValues == 0) {
+            return INVALID_ENTRY_VALUE;
+        }
+
+        if (mNumberOfEntryValues == 1) {
+            entryIndex = 0;
+        }
+
+        int offset;
+        if (entryIndex < mNumberOfEntryValues) {
+            // offset = table prefix(4) + entry value count(4) + size of entry * entry index
+            offset = mHeaderByteOffsetToRead + (mEntryValueSizeInBytes * entryIndex);
+        } else {
+            return INVALID_ENTRY_VALUE;
+        }
+
+        return getValueInternal(mSharedDataToRead, mEntryValueSizeInBytes, offset);
+    }
+
+    // Entry lists to be written to a byte buffer.
+    public List<Integer> getEntryValuesToWrite() {
+        return mEntryValuesToWrite;
     }
 
     /**
@@ -46,6 +132,20 @@
         return mTablePrefix;
     }
 
+    /**
+     * Returns the number of entry values.
+     */
+    public int getNumberOfEntryValues() {
+        return mNumberOfEntryValues;
+    }
+
+    /**
+     * Returns the size of entry value in Bytes.
+     */
+    public int getEntryValueSizeInBytes() {
+        return mEntryValueSizeInBytes;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) {
@@ -55,18 +155,22 @@
             return false;
         }
         SuffixTableSharedData that = (SuffixTableSharedData) o;
-        return mTablePrefix == that.mTablePrefix;
+        return mTablePrefix == that.mTablePrefix
+                && mNumberOfEntryValues == that.mNumberOfEntryValues
+                && mEntryValuesToWrite.equals(that.mEntryValuesToWrite);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mTablePrefix);
+        return Objects.hash(mTablePrefix, mNumberOfEntryValues, mEntryValuesToWrite);
     }
 
     @Override
     public String toString() {
         return "SuffixTableSharedData{"
                 + "mTablePrefix=" + mTablePrefix
+                + "mNumberOfEntries=" + mNumberOfEntryValues
+                + "mEntryValuesToWrite=" + mEntryValuesToWrite
                 + '}';
     }
 
@@ -82,4 +186,21 @@
             throw new RuntimeException(e);
         }
     }
+
+    private int getValueInternal(TypedData buffer, int valueSizeBytes, int byteOffset) {
+        if (byteOffset < 0) {
+            throw new IllegalArgumentException(
+                    "byteOffset=" + byteOffset + " must not be negative");
+        }
+
+        // High bytes read first.
+        int value = 0;
+        int bytesRead = 0;
+        while (bytesRead++ < valueSizeBytes) {
+            value <<= Byte.SIZE;
+            value |= buffer.getUnsignedByte(byteOffset++);
+        }
+
+        return value;
+    }
 }
diff --git a/utils/satellite/s2storage/src/test/java/com/android/telephony/sats2range/SatS2RangeFileFormatTest.java b/utils/satellite/s2storage/src/test/java/com/android/telephony/sats2range/SatS2RangeFileFormatTest.java
index 80ef467..65da1e4 100644
--- a/utils/satellite/s2storage/src/test/java/com/android/telephony/sats2range/SatS2RangeFileFormatTest.java
+++ b/utils/satellite/s2storage/src/test/java/com/android/telephony/sats2range/SatS2RangeFileFormatTest.java
@@ -67,10 +67,12 @@
         int suffixBitCount = 16;
         int suffixTableBlockIdOffset = 5;
         int suffixTableEntryBitCount = 24;
+        int suffixTableEntryValueSizeInBytes = 4;
+        int versionNumber = 2;
         boolean isAllowedList = false;
         SatS2RangeFileFormat satS2RangeFileFormat = new SatS2RangeFileFormat(s2Level,
                 prefixBitCount, suffixBitCount, suffixTableBlockIdOffset, suffixTableEntryBitCount,
-                isAllowedList);
+                isAllowedList, suffixTableEntryValueSizeInBytes, versionNumber);
 
         assertEquals(2, satS2RangeFileFormat.calculateRangeLength(
                 cellId(s2Level, 0, 0), cellId(s2Level, 0, 2)));
@@ -92,9 +94,11 @@
         int suffixTableBlockIdOffset = 5;
         int suffixTableEntryBitCount = 24;
         boolean isAllowedList = true;
+        int suffixTableEntryValueSizeInBytes = 4;
+        int versionNumber = 2;
         SatS2RangeFileFormat satS2RangeFileFormat = new SatS2RangeFileFormat(s2Level,
                 prefixBitCount, suffixBitCount, suffixTableBlockIdOffset, suffixTableEntryBitCount,
-                isAllowedList);
+                isAllowedList, suffixTableEntryValueSizeInBytes, versionNumber);
 
         // Too many bits for prefixValue
         assertThrows(IllegalArgumentException.class,
@@ -127,9 +131,11 @@
         int suffixTableBlockIdOffset = 5;
         int suffixTableEntryBitCount = 24;
         boolean isAllowedList = true;
+        int suffixTableEntryValueSizeInBytes = 4;
+        int versionNumber = 2;
         SatS2RangeFileFormat satS2RangeFileFormat = new SatS2RangeFileFormat(s2Level,
                 prefixBitCount, suffixBitCount, suffixTableBlockIdOffset, suffixTableEntryBitCount,
-                isAllowedList);
+                isAllowedList, suffixTableEntryValueSizeInBytes, versionNumber);
 
         assertEquals(0, satS2RangeFileFormat.extractFaceIdFromPrefix(0b00000000000));
         assertEquals(5, satS2RangeFileFormat.extractFaceIdFromPrefix(0b10100000000));
@@ -147,9 +153,11 @@
         int suffixTableBlockIdOffset = 5;
         int suffixTableEntryBitCount = 24;
         boolean isAllowedList = true;
+        int suffixTableEntryValueSizeInBytes = 4;
+        int versionNumber = 2;
         SatS2RangeFileFormat satS2RangeFileFormat = new SatS2RangeFileFormat(s2Level,
                 prefixBitCount, suffixBitCount, suffixTableBlockIdOffset, suffixTableEntryBitCount,
-                isAllowedList);
+                isAllowedList, suffixTableEntryValueSizeInBytes, versionNumber);
 
         // Too many bits for rangeLength
         assertThrows(IllegalArgumentException.class,
@@ -161,6 +169,33 @@
         assertTrue(satS2RangeFileFormat.isAllowedList());
     }
 
+    @Test
+    public void extractEntryValueByteCount() {
+        int s2Level = 12;
+        int prefixBitCount = 11;
+        int suffixBitCount = 16;
+        int suffixTableBlockIdOffset = 5;
+        int suffixTableEntryBitCount = 24;
+        boolean isAllowedList = true;
+        final int[] suffixTableEntryValueSizeInBytes = {5};
+        int versionNumber = 1;
+
+        // Table entry byte count exceeds BYTE range.
+        assertThrows(IllegalArgumentException.class,
+                () -> new SatS2RangeFileFormat(s2Level, prefixBitCount, suffixBitCount,
+                        suffixTableBlockIdOffset, suffixTableEntryBitCount, isAllowedList,
+                        suffixTableEntryValueSizeInBytes[0], versionNumber));
+
+        suffixTableEntryValueSizeInBytes[0] = 1;
+        SatS2RangeFileFormat satS2RangeFileFormat = new SatS2RangeFileFormat(s2Level,
+                prefixBitCount, suffixBitCount, suffixTableBlockIdOffset, suffixTableEntryBitCount,
+                isAllowedList, suffixTableEntryValueSizeInBytes[0], versionNumber);
+
+        assertEquals(suffixTableEntryValueSizeInBytes[0],
+                satS2RangeFileFormat.getEntryValueSizeInBytes());
+        assertEquals(versionNumber, satS2RangeFileFormat.getVersionNumber());
+    }
+
     private static int maxValForBits(int bits) {
         return intPow2(bits) - 1;
     }
diff --git a/utils/satellite/s2storage/src/test/java/com/android/telephony/sats2range/SuffixTableSharedDataTest.java b/utils/satellite/s2storage/src/test/java/com/android/telephony/sats2range/SuffixTableSharedDataTest.java
index 2baefa9..ee21626 100644
--- a/utils/satellite/s2storage/src/test/java/com/android/telephony/sats2range/SuffixTableSharedDataTest.java
+++ b/utils/satellite/s2storage/src/test/java/com/android/telephony/sats2range/SuffixTableSharedDataTest.java
@@ -18,11 +18,18 @@
 
 import static org.junit.Assert.assertEquals;
 
+import com.android.storage.block.read.BlockData;
+import com.android.telephony.sats2range.read.SatS2RangeFileFormat;
 import com.android.telephony.sats2range.read.SuffixTableSharedData;
 import com.android.telephony.sats2range.write.SuffixTableSharedDataWriter;
 
 import org.junit.Test;
 
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
 /** Tests for {@link SuffixTableSharedData} and {@link SuffixTableSharedDataWriter}. */
 public class SuffixTableSharedDataTest {
     @Test
@@ -33,5 +40,106 @@
 
         assertEquals(sharedData, SuffixTableSharedData.fromBytes(bytes));
     }
-}
 
+    @Test
+    public void testSuffixTableSharedDataWithEntryValues() {
+        int prefix = 321;
+        int entryValueSizeInBytes = 1;
+        List<Integer> entryValues = new ArrayList<>(Arrays.asList(0x01, 0x7F, 0xFF));
+        int versionNumber = 1;
+
+        // Verify whether fromTypedData returns correct SuffixTableSharedData when entryByte is 1
+        verifySharedData(prefix, entryValueSizeInBytes, entryValues.size(), entryValues,
+                versionNumber);
+
+        // Verify when entryValueSizeInBytes is 2
+        entryValueSizeInBytes = 2;
+        entryValues = new ArrayList<>(Arrays.asList(0x001, 0x5FFF, 0xAFFF, 0xFFFF));
+        verifySharedData(prefix, entryValueSizeInBytes, entryValues.size(), entryValues,
+                versionNumber);
+
+        // Verify when entryValueSizeInBytes is 3
+        entryValueSizeInBytes = 3;
+        entryValues = new ArrayList<>(
+                Arrays.asList(0x000001, 0x4FFFFF, 0x8FFFFF, 0xBFFFFF, 0xFFFFFF));
+        verifySharedData(prefix, entryValueSizeInBytes, entryValues.size(), entryValues,
+                versionNumber);
+
+        // Verify when entryValueSizeInBytes is 4, max int value is 0x7FFFFFFF.
+        // ConfigID is supported up to 0x7FFFFFFF for now.
+        entryValueSizeInBytes = 4;
+        entryValues = new ArrayList<>(
+                Arrays.asList(0x00000001, 0x2FFFFFFF, 0x3FFFFFFF, 0x4FFFFFFF, 0x5FFFFFFF,
+                        0x6FFFFFFF, 0x7FFFFFFF));
+        verifySharedData(prefix, entryValueSizeInBytes, entryValues.size(), entryValues,
+                versionNumber);
+
+        // Verify when every entry has same value.
+        entryValues = new ArrayList<>(
+                Arrays.asList(0x3FFFFFFF, 0x3FFFFFFF, 0x3FFFFFFF, 0x3FFFFFFF, 0x3FFFFFFF,
+                        0x3FFFFFFF, 0x3FFFFFFF));
+        verifySharedData(prefix, entryValueSizeInBytes, entryValues.size(), entryValues,
+                versionNumber);
+
+        // Verify when entry is empty
+        // entryValueSizeInBytes is set as 4, but there is no entry list
+        entryValues = new ArrayList<>(List.of());
+        verifySharedData(prefix, entryValueSizeInBytes, entryValues.size(), entryValues,
+                versionNumber);
+        // entryValueSizeInBytes is 0, no entry list
+        entryValueSizeInBytes = 0;
+        verifySharedData(prefix, entryValueSizeInBytes, entryValues.size(), entryValues,
+                versionNumber);
+    }
+
+    private BlockData createBlockedDataFromByteBuffer(int prefix,
+            List<Integer> entryValues, SatS2RangeFileFormat fileFormat) {
+        SuffixTableSharedData sharedDataToWrite = new SuffixTableSharedData(prefix, entryValues,
+                fileFormat);
+        ByteBuffer byteBuffer = ByteBuffer.wrap(
+                SuffixTableSharedDataWriter.toBytes(sharedDataToWrite));
+        return new BlockData(byteBuffer.asReadOnlyBuffer());
+    }
+
+    private void verifySharedData(int expectedTablePrefix, int expectedEntryValueSizeInBytes,
+            int expectedNumberOfEntryValues, List<Integer> expectedEntryValues, int versionNumber) {
+        SatS2RangeFileFormat fileFormat = createSatS2RangeFileFormat(expectedEntryValueSizeInBytes,
+                versionNumber);
+        BlockData blockData = createBlockedDataFromByteBuffer(
+                expectedTablePrefix, expectedEntryValues, fileFormat);
+        SuffixTableSharedData sharedData = SuffixTableSharedData.fromTypedData(blockData,
+                fileFormat);
+
+        assertEquals(expectedTablePrefix, sharedData.getTablePrefix());
+        if (!expectedEntryValues.isEmpty()) {
+            assertEquals(expectedEntryValueSizeInBytes, sharedData.getEntryValueSizeInBytes());
+        } else {
+            assertEquals(0, sharedData.getEntryValueSizeInBytes());
+        }
+
+        // If every entry has same value, block data contains only 1 entry info
+        if (expectedEntryValues.stream().distinct().count() == 1) {
+            assertEquals(3 * Integer.BYTES, blockData.getSize());
+            // Verify whether the entry value count has been set to 1.
+            assertEquals(1, sharedData.getNumberOfEntryValues());
+        } else {
+            assertEquals(expectedNumberOfEntryValues, sharedData.getNumberOfEntryValues());
+        }
+        for (int i = 0; i < expectedNumberOfEntryValues; i++) {
+            assertEquals((int) expectedEntryValues.get(i), sharedData.getEntryValue(i));
+        }
+    }
+
+    private SatS2RangeFileFormat createSatS2RangeFileFormat(int entryByteCount, int versionNumber) {
+        int s2Level = 12;
+        int prefixBitCount = 11;
+        int suffixBitCount = 16;
+        int suffixTableBlockIdOffset = 5;
+        int suffixTableEntryBitCount = 24;
+        boolean isAllowedList = true;
+
+        return new SatS2RangeFileFormat(s2Level,
+                prefixBitCount, suffixBitCount, suffixTableBlockIdOffset, suffixTableEntryBitCount,
+                isAllowedList, entryByteCount, versionNumber);
+    }
+}
diff --git a/utils/satellite/s2storage/src/write/java/com/android/telephony/sats2range/write/HeaderBlockWriter.java b/utils/satellite/s2storage/src/write/java/com/android/telephony/sats2range/write/HeaderBlockWriter.java
index d4e9310..0435922 100644
--- a/utils/satellite/s2storage/src/write/java/com/android/telephony/sats2range/write/HeaderBlockWriter.java
+++ b/utils/satellite/s2storage/src/write/java/com/android/telephony/sats2range/write/HeaderBlockWriter.java
@@ -63,6 +63,8 @@
             tos.writeUnsignedByte(mFileFormat.getSuffixTableBlockIdOffset());
             tos.writeUnsignedByte(mFileFormat.isAllowedList()
                     ? HeaderBlock.TRUE : HeaderBlock.FALSE);
+            tos.writeUnsignedByte(mFileFormat.getEntryValueSizeInBytes());
+            tos.writeInt(mFileFormat.getVersionNumber());
         }
 
         FileChannel fileChannel = FileChannel.open(mFile.toPath(), StandardOpenOption.READ);
diff --git a/utils/satellite/s2storage/src/write/java/com/android/telephony/sats2range/write/SuffixTableSharedDataWriter.java b/utils/satellite/s2storage/src/write/java/com/android/telephony/sats2range/write/SuffixTableSharedDataWriter.java
index 5499148..a739e5e 100644
--- a/utils/satellite/s2storage/src/write/java/com/android/telephony/sats2range/write/SuffixTableSharedDataWriter.java
+++ b/utils/satellite/s2storage/src/write/java/com/android/telephony/sats2range/write/SuffixTableSharedDataWriter.java
@@ -21,21 +21,37 @@
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.util.List;
 
 /**
  * Converts a {@link SuffixTableSharedData} to a byte[] for writing.
  * See also {@link SuffixTableSharedData#fromBytes(byte[])}.
  */
 public final class SuffixTableSharedDataWriter {
-
+    private static final int BUFFER_SIZE = (int) Math.pow(2, 20);
     private SuffixTableSharedDataWriter() {
     }
 
     /** Returns the byte[] for the supplied {@link SuffixTableSharedData} */
     public static byte[] toBytes(SuffixTableSharedData suffixTableSharedData) {
+        int entryValueSizeInBytes = suffixTableSharedData.getEntryValueSizeInBytes();
+        List<Integer> entryValues = suffixTableSharedData.getEntryValuesToWrite();
+        // If every entry has same value, compress to save memory
+        int numberOfEntryValues =
+                entryValues.stream().distinct().count() == 1 ? 1 : entryValues.size();
+
         try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
-                TypedOutputStream tos = new TypedOutputStream(baos)) {
+                TypedOutputStream tos = new TypedOutputStream(baos, BUFFER_SIZE)) {
             tos.writeInt(suffixTableSharedData.getTablePrefix());
+
+            if (entryValueSizeInBytes > 0 && !entryValues.isEmpty()) {
+                tos.writeInt(numberOfEntryValues);
+                for (int i = 0; i < numberOfEntryValues; i++) {
+                    // ConfigId is supported up to 0x7FFFFFFF
+                    tos.writeVarByteValue(entryValueSizeInBytes, entryValues.get(i));
+                }
+            }
+
             tos.flush();
             return baos.toByteArray();
         } catch (IOException e) {