Implement storage files in Java

This change implements storage files in java to remove the dependencies
on JNI.

Bug: 352078117
Test: atest aconfig_storage_file.test.java

Change-Id: I34438576d09d0aa31eabc60f472e71cebf1552a0
diff --git a/tools/aconfig/TEST_MAPPING b/tools/aconfig/TEST_MAPPING
index 448d8cf..15e4187 100644
--- a/tools/aconfig/TEST_MAPPING
+++ b/tools/aconfig/TEST_MAPPING
@@ -98,6 +98,10 @@
     {
       // aconfig_storage file cpp integration tests
       "name": "aconfig_storage_file.test.cpp"
+    },
+    {
+      // aconfig_storage file java integration tests
+      "name": "aconfig_storage_file.test.java"
     }
   ],
   "postsubmit": [
diff --git a/tools/aconfig/aconfig_storage_file/Android.bp b/tools/aconfig/aconfig_storage_file/Android.bp
index e066e31..3859194 100644
--- a/tools/aconfig/aconfig_storage_file/Android.bp
+++ b/tools/aconfig/aconfig_storage_file/Android.bp
@@ -137,3 +137,12 @@
     min_sdk_version: "29",
     double_loadable: true,
 }
+
+// storage file parse api java cc_library
+java_library {
+    name: "aconfig_storage_file_java",
+    srcs: [
+        "srcs/**/*.java",
+    ],
+    sdk_version: "core_current",
+}
\ No newline at end of file
diff --git a/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/AconfigStorageException.java b/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/AconfigStorageException.java
new file mode 100644
index 0000000..86a75f2
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/AconfigStorageException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.aconfig.storage;
+
+public class AconfigStorageException extends RuntimeException {
+    public AconfigStorageException(String msg) {
+        super(msg);
+    }
+
+    public AconfigStorageException(String msg, Throwable cause) {
+        super(msg, cause);
+    }
+}
diff --git a/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/ByteBufferReader.java b/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/ByteBufferReader.java
new file mode 100644
index 0000000..1c72364
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/ByteBufferReader.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.aconfig.storage;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+
+public class ByteBufferReader {
+
+    private ByteBuffer mByteBuffer;
+
+    public ByteBufferReader(ByteBuffer byteBuffer) {
+        this.mByteBuffer = byteBuffer;
+        this.mByteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+    }
+
+    public int readByte() {
+        return Byte.toUnsignedInt(mByteBuffer.get());
+    }
+
+    public int readShort() {
+        return Short.toUnsignedInt(mByteBuffer.getShort());
+    }
+
+    public int readInt() {
+        return this.mByteBuffer.getInt();
+    }
+
+    public String readString() {
+        int length = readInt();
+        byte[] bytes = new byte[length];
+        mByteBuffer.get(bytes, 0, length);
+        return new String(bytes, StandardCharsets.UTF_8);
+    }
+
+    public void position(int newPosition) {
+        mByteBuffer.position(newPosition);
+    }
+}
diff --git a/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/FileType.java b/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/FileType.java
new file mode 100644
index 0000000..b0b1b9b
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/FileType.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.aconfig.storage;
+
+public enum FileType {
+    PACKAGE_MAP(0),
+    FLAG_MAP(1),
+    FLAG_VAL(2),
+    FLAG_INFO(3);
+
+    public final int type;
+
+    FileType(int type) {
+        this.type = type;
+    }
+
+    public static FileType fromInt(int index) {
+        switch (index) {
+            case 0:
+                return PACKAGE_MAP;
+            case 1:
+                return FLAG_MAP;
+            case 2:
+                return FLAG_VAL;
+            case 3:
+                return FLAG_INFO;
+            default:
+                return null;
+        }
+    }
+}
diff --git a/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/FlagTable.java b/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/FlagTable.java
new file mode 100644
index 0000000..e85fdee
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/FlagTable.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.aconfig.storage;
+
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+public class FlagTable {
+
+    private Header mHeader;
+    private Map<String, Node> mNodeMap;
+
+    public static FlagTable fromBytes(ByteBuffer bytes) {
+        FlagTable flagTable = new FlagTable();
+        ByteBufferReader reader = new ByteBufferReader(bytes);
+        Header header = Header.fromBytes(reader);
+        flagTable.mHeader = header;
+        flagTable.mNodeMap = new HashMap(TableUtils.getTableSize(header.mNumFlags));
+        reader.position(header.mNodeOffset);
+        for (int i = 0; i < header.mNumFlags; i++) {
+            Node node = Node.fromBytes(reader);
+            flagTable.mNodeMap.put(makeKey(node.mPackageId, node.mFlagName), node);
+        }
+        return flagTable;
+    }
+
+    public Node get(int packageId, String flagName) {
+        return mNodeMap.get(makeKey(packageId, flagName));
+    }
+
+    public Header getHeader() {
+        return mHeader;
+    }
+
+    private static String makeKey(int packageId, String flagName) {
+        StringBuilder ret = new StringBuilder();
+        return ret.append(packageId).append('/').append(flagName).toString();
+    }
+
+    public static class Header {
+
+        private int mVersion;
+        private String mContainer;
+        private FileType mFileType;
+        private int mFileSize;
+        private int mNumFlags;
+        private int mBucketOffset;
+        private int mNodeOffset;
+
+        public static Header fromBytes(ByteBufferReader reader) {
+            Header header = new Header();
+            header.mVersion = reader.readInt();
+            header.mContainer = reader.readString();
+            header.mFileType = FileType.fromInt(reader.readByte());
+            header.mFileSize = reader.readInt();
+            header.mNumFlags = reader.readInt();
+            header.mBucketOffset = reader.readInt();
+            header.mNodeOffset = reader.readInt();
+
+            if (header.mFileType != FileType.FLAG_MAP) {
+                throw new AconfigStorageException("binary file is not a flag map");
+            }
+
+            return header;
+        }
+
+        public int getVersion() {
+            return mVersion;
+        }
+
+        public String getContainer() {
+            return mContainer;
+        }
+
+        public FileType getFileType() {
+            return mFileType;
+        }
+
+        public int getFileSize() {
+            return mFileSize;
+        }
+
+        public int getNumFlags() {
+            return mNumFlags;
+        }
+
+        public int getBucketOffset() {
+            return mBucketOffset;
+        }
+
+        public int getNodeOffset() {
+            return mNodeOffset;
+        }
+    }
+
+    public static class Node {
+
+        private String mFlagName;
+        private FlagType mFlagType;
+        private int mPackageId;
+        private int mFlagIndex;
+        private int mNextOffset;
+
+        public static Node fromBytes(ByteBufferReader reader) {
+            Node node = new Node();
+            node.mPackageId = reader.readInt();
+            node.mFlagName = reader.readString();
+            node.mFlagType = FlagType.fromInt(reader.readShort());
+            node.mFlagIndex = reader.readShort();
+            node.mNextOffset = reader.readInt();
+            node.mNextOffset = node.mNextOffset == 0 ? -1 : node.mNextOffset;
+            return node;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mFlagName, mFlagType, mPackageId, mFlagIndex, mNextOffset);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+
+            if (obj == null || !(obj instanceof Node)) {
+                return false;
+            }
+
+            Node other = (Node) obj;
+            return Objects.equals(mFlagName, other.mFlagName)
+                    && Objects.equals(mFlagType, other.mFlagType)
+                    && mPackageId == other.mPackageId
+                    && mFlagIndex == other.mFlagIndex
+                    && mNextOffset == other.mNextOffset;
+        }
+
+        public String getFlagName() {
+            return mFlagName;
+        }
+
+        public FlagType getFlagType() {
+            return mFlagType;
+        }
+
+        public int getPackageId() {
+            return mPackageId;
+        }
+
+        public int getFlagIndex() {
+            return mFlagIndex;
+        }
+
+        public int getNextOffset() {
+            return mNextOffset;
+        }
+    }
+}
diff --git a/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/FlagType.java b/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/FlagType.java
new file mode 100644
index 0000000..385e2d9
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/FlagType.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.aconfig.storage;
+
+public enum FlagType {
+    ReadWriteBoolean (0),
+    ReadOnlyBoolean(1),
+    FixedReadOnlyBoolean(2);
+
+    public final int type;
+
+    FlagType(int type) {
+        this.type = type;
+    }
+
+    public static FlagType fromInt(int index) {
+        switch (index) {
+            case 0:
+                return ReadWriteBoolean;
+            case 1:
+                return ReadOnlyBoolean;
+            case 2:
+                return FixedReadOnlyBoolean;
+            default:
+                return null;
+        }
+    }
+}
diff --git a/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/FlagValueList.java b/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/FlagValueList.java
new file mode 100644
index 0000000..0ddc147
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/FlagValueList.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.aconfig.storage;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+public class FlagValueList {
+
+    private Header mHeader;
+    private List<Boolean> mList;
+
+    private int mSize;
+
+    public static FlagValueList fromBytes(ByteBuffer bytes) {
+        FlagValueList flagValueList = new FlagValueList();
+        ByteBufferReader reader = new ByteBufferReader(bytes);
+        Header header = Header.fromBytes(reader);
+        flagValueList.mHeader = header;
+        flagValueList.mList = new ArrayList(header.mNumFlags);
+        reader.position(header.mBooleanValueOffset);
+        for (int i = 0; i < header.mNumFlags; i++) {
+            boolean val = reader.readByte() == 1;
+            flagValueList.mList.add(val);
+        }
+        flagValueList.mSize = flagValueList.mList.size();
+        return flagValueList;
+    }
+
+    public boolean get(int index) {
+        return mList.get(index);
+    }
+
+    public Header getHeader() {
+        return mHeader;
+    }
+
+    public int size() {
+        return mSize;
+    }
+
+    public static class Header {
+
+        private int mVersion;
+        private String mContainer;
+        private FileType mFileType;
+        private int mFileSize;
+        private int mNumFlags;
+        private int mBooleanValueOffset;
+
+        public static Header fromBytes(ByteBufferReader reader) {
+            Header header = new Header();
+            header.mVersion = reader.readInt();
+            header.mContainer = reader.readString();
+            header.mFileType = FileType.fromInt(reader.readByte());
+            header.mFileSize = reader.readInt();
+            header.mNumFlags = reader.readInt();
+            header.mBooleanValueOffset = reader.readInt();
+
+            if (header.mFileType != FileType.FLAG_VAL) {
+                throw new AconfigStorageException("binary file is not a flag value file");
+            }
+
+            return header;
+        }
+
+        public int getVersion() {
+            return mVersion;
+        }
+
+        public String getContainer() {
+            return mContainer;
+        }
+
+        public FileType getFileType() {
+            return mFileType;
+        }
+
+        public int getFileSize() {
+            return mFileSize;
+        }
+
+        public int getNumFlags() {
+            return mNumFlags;
+        }
+
+        public int getBooleanValueOffset() {
+            return mBooleanValueOffset;
+        }
+    }
+}
diff --git a/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/PackageTable.java b/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/PackageTable.java
new file mode 100644
index 0000000..d04e1ac
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/PackageTable.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.aconfig.storage;
+
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+public class PackageTable {
+
+    private Header mHeader;
+    private Map<String, Node> mNodeMap;
+
+    public static PackageTable fromBytes(ByteBuffer bytes) {
+        PackageTable packageTable = new PackageTable();
+        ByteBufferReader reader = new ByteBufferReader(bytes);
+        Header header = Header.fromBytes(reader);
+        packageTable.mHeader = header;
+        packageTable.mNodeMap = new HashMap(TableUtils.getTableSize(header.mNumPackages));
+        reader.position(header.mNodeOffset);
+        for (int i = 0; i < header.mNumPackages; i++) {
+            Node node = Node.fromBytes(reader);
+            packageTable.mNodeMap.put(node.mPackageName, node);
+        }
+        return packageTable;
+    }
+
+    public Node get(String packageName) {
+        return mNodeMap.get(packageName);
+    }
+
+    public Header getHeader() {
+        return mHeader;
+    }
+
+    public static class Header {
+
+        private int mVersion;
+        private String mContainer;
+        private FileType mFileType;
+        private int mFileSize;
+        private int mNumPackages;
+        private int mBucketOffset;
+        private int mNodeOffset;
+
+        public static Header fromBytes(ByteBufferReader reader) {
+            Header header = new Header();
+            header.mVersion = reader.readInt();
+            header.mContainer = reader.readString();
+            header.mFileType = FileType.fromInt(reader.readByte());
+            header.mFileSize = reader.readInt();
+            header.mNumPackages = reader.readInt();
+            header.mBucketOffset = reader.readInt();
+            header.mNodeOffset = reader.readInt();
+
+            if (header.mFileType != FileType.PACKAGE_MAP) {
+                throw new AconfigStorageException("binary file is not a package map");
+            }
+
+            return header;
+        }
+
+        public int getVersion() {
+            return mVersion;
+        }
+
+        public String getContainer() {
+            return mContainer;
+        }
+
+        public FileType getFileType() {
+            return mFileType;
+        }
+
+        public int getFileSize() {
+            return mFileSize;
+        }
+
+        public int getNumPackages() {
+            return mNumPackages;
+        }
+
+        public int getBucketOffset() {
+            return mBucketOffset;
+        }
+
+        public int getNodeOffset() {
+            return mNodeOffset;
+        }
+    }
+
+    public static class Node {
+
+        private String mPackageName;
+        private int mPackageId;
+        private int mBooleanStartIndex;
+        private int mNextOffset;
+
+        public static Node fromBytes(ByteBufferReader reader) {
+            Node node = new Node();
+            node.mPackageName = reader.readString();
+            node.mPackageId = reader.readInt();
+            node.mBooleanStartIndex = reader.readInt();
+            node.mNextOffset = reader.readInt();
+            node.mNextOffset = node.mNextOffset == 0 ? -1 : node.mNextOffset;
+            return node;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mPackageName, mPackageId, mBooleanStartIndex, mNextOffset);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+
+            if (obj == null || !(obj instanceof Node)) {
+                return false;
+            }
+
+            Node other = (Node) obj;
+            return Objects.equals(mPackageName, other.mPackageName)
+                    && mPackageId == other.mPackageId
+                    && mBooleanStartIndex == other.mBooleanStartIndex
+                    && mNextOffset == other.mNextOffset;
+        }
+
+        public String getPackageName() {
+            return mPackageName;
+        }
+
+        public int getPackageId() {
+            return mPackageId;
+        }
+
+        public int getBooleanStartIndex() {
+            return mBooleanStartIndex;
+        }
+
+        public int getNextOffset() {
+            return mNextOffset;
+        }
+    }
+}
diff --git a/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/TableUtils.java b/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/TableUtils.java
new file mode 100644
index 0000000..714b53b
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_file/srcs/android/aconfig/storage/TableUtils.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.aconfig.storage;
+
+public class TableUtils {
+
+    private static final int[] HASH_PRIMES =
+            new int[] {
+                7,
+                17,
+                29,
+                53,
+                97,
+                193,
+                389,
+                769,
+                1543,
+                3079,
+                6151,
+                12289,
+                24593,
+                49157,
+                98317,
+                196613,
+                393241,
+                786433,
+                1572869,
+                3145739,
+                6291469,
+                12582917,
+                25165843,
+                50331653,
+                100663319,
+                201326611,
+                402653189,
+                805306457,
+                1610612741
+            };
+
+    public static int getTableSize(int numEntries) {
+        for (int i : HASH_PRIMES) {
+            if (i < 2 * numEntries) continue;
+            return i;
+        }
+        throw new AconfigStorageException("Number of items in a hash table exceeds limit");
+    }
+}
diff --git a/tools/aconfig/aconfig_storage_file/tests/Android.bp b/tools/aconfig/aconfig_storage_file/tests/Android.bp
index 26b7800..c33127f 100644
--- a/tools/aconfig/aconfig_storage_file/tests/Android.bp
+++ b/tools/aconfig/aconfig_storage_file/tests/Android.bp
@@ -1,4 +1,3 @@
-
 cc_test {
     name: "aconfig_storage_file.test.cpp",
     team: "trendy_team_android_core_experiments",
@@ -21,3 +20,27 @@
         "general-tests",
     ],
 }
+
+android_test {
+    name: "aconfig_storage_file.test.java",
+    team: "trendy_team_android_core_experiments",
+    srcs: [
+        "srcs/**/*.java",
+    ],
+    static_libs: [
+        "aconfig_storage_file_java",
+        "androidx.test.runner",
+        "junit",
+    ],
+    sdk_version: "test_current",
+    test_config: "AndroidStorageJaveTest.xml",
+    data: [
+        "package.map",
+        "flag.map",
+        "flag.val",
+        "flag.info",
+    ],
+    test_suites: [
+        "general-tests",
+    ],
+}
diff --git a/tools/aconfig/aconfig_storage_file/tests/AndroidManifest.xml b/tools/aconfig/aconfig_storage_file/tests/AndroidManifest.xml
new file mode 100644
index 0000000..5e01879
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_file/tests/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.aconfig.storage.test">
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="android.aconfig.storage.test" />
+
+</manifest>
diff --git a/tools/aconfig/aconfig_storage_file/tests/AndroidStorageJaveTest.xml b/tools/aconfig/aconfig_storage_file/tests/AndroidStorageJaveTest.xml
new file mode 100644
index 0000000..2d52d44
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_file/tests/AndroidStorageJaveTest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<configuration description="Test aconfig storage java tests">
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="aconfig_storage_file.test.java.apk" />
+    </target_preparer>
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+        <option name="cleanup" value="true" />
+        <option name="push" value="package.map->/data/local/tmp/aconfig_storage_file_test_java/testdata/package.map" />
+        <option name="push" value="flag.map->/data/local/tmp/aconfig_storage_file_test_java/testdata/flag.map" />
+        <option name="push" value="flag.val->/data/local/tmp/aconfig_storage_file_test_java/testdata/flag.val" />
+        <option name="push" value="flag.info->/data/local/tmp/aconfig_storage_file_test_java/testdata/flag.info" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.aconfig.storage.test" />
+        <option name="runtime-hint" value="1m" />
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/tools/aconfig/aconfig_storage_file/tests/srcs/ByteBufferReaderTest.java b/tools/aconfig/aconfig_storage_file/tests/srcs/ByteBufferReaderTest.java
new file mode 100644
index 0000000..66a8166
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_file/tests/srcs/ByteBufferReaderTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.aconfig.storage.test;
+
+import static org.junit.Assert.assertEquals;
+
+import android.aconfig.storage.ByteBufferReader;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+
+@RunWith(JUnit4.class)
+public class ByteBufferReaderTest {
+
+    @Test
+    public void testReadByte() {
+        ByteBuffer buffer = ByteBuffer.allocate(1);
+        byte expect = 10;
+        buffer.put(expect).rewind();
+
+        ByteBufferReader reader = new ByteBufferReader(buffer);
+        assertEquals(expect, reader.readByte());
+    }
+
+    @Test
+    public void testReadShort() {
+        ByteBuffer buffer = ByteBuffer.allocate(4);
+        buffer.order(ByteOrder.LITTLE_ENDIAN);
+        short expect = Short.MAX_VALUE;
+        buffer.putShort(expect).rewind();
+
+        ByteBufferReader reader = new ByteBufferReader(buffer);
+        assertEquals(expect, reader.readShort());
+    }
+
+    @Test
+    public void testReadInt() {
+        ByteBuffer buffer = ByteBuffer.allocate(4);
+        buffer.order(ByteOrder.LITTLE_ENDIAN);
+        int expect = 10000;
+        buffer.putInt(expect).rewind();
+
+        ByteBufferReader reader = new ByteBufferReader(buffer);
+        assertEquals(expect, reader.readInt());
+    }
+
+    @Test
+    public void testReadString() {
+        String expect = "test read string";
+        byte[] bytes = expect.getBytes(StandardCharsets.UTF_8);
+
+        ByteBuffer buffer = ByteBuffer.allocate(expect.length() * 2 + 4);
+        buffer.order(ByteOrder.LITTLE_ENDIAN);
+        buffer.putInt(expect.length()).put(bytes).rewind();
+
+        ByteBufferReader reader = new ByteBufferReader(buffer);
+
+        assertEquals(expect, reader.readString());
+    }
+}
diff --git a/tools/aconfig/aconfig_storage_file/tests/srcs/FlagTableTest.java b/tools/aconfig/aconfig_storage_file/tests/srcs/FlagTableTest.java
new file mode 100644
index 0000000..fd40d4c
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_file/tests/srcs/FlagTableTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.aconfig.storage.test;
+
+import static org.junit.Assert.assertEquals;
+
+import android.aconfig.storage.FileType;
+import android.aconfig.storage.FlagTable;
+import android.aconfig.storage.FlagType;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class FlagTableTest {
+
+    @Test
+    public void testFlagTable_rightHeader() throws Exception {
+        FlagTable flagTable = FlagTable.fromBytes(TestDataUtils.getTestFlagMapByteBuffer());
+        FlagTable.Header header = flagTable.getHeader();
+        assertEquals(1, header.getVersion());
+        assertEquals("mockup", header.getContainer());
+        assertEquals(FileType.FLAG_MAP, header.getFileType());
+        assertEquals(321, header.getFileSize());
+        assertEquals(8, header.getNumFlags());
+        assertEquals(31, header.getBucketOffset());
+        assertEquals(99, header.getNodeOffset());
+    }
+
+    @Test
+    public void testFlagTable_rightNode() throws Exception {
+        FlagTable flagTable = FlagTable.fromBytes(TestDataUtils.getTestFlagMapByteBuffer());
+
+        FlagTable.Node node1 = flagTable.get(0, "enabled_ro");
+        FlagTable.Node node2 = flagTable.get(0, "enabled_rw");
+        FlagTable.Node node3 = flagTable.get(2, "enabled_rw");
+        FlagTable.Node node4 = flagTable.get(1, "disabled_rw");
+        FlagTable.Node node5 = flagTable.get(1, "enabled_fixed_ro");
+        FlagTable.Node node6 = flagTable.get(1, "enabled_ro");
+        FlagTable.Node node7 = flagTable.get(2, "enabled_fixed_ro");
+        FlagTable.Node node8 = flagTable.get(0, "disabled_rw");
+
+        assertEquals("enabled_ro", node1.getFlagName());
+        assertEquals("enabled_rw", node2.getFlagName());
+        assertEquals("enabled_rw", node3.getFlagName());
+        assertEquals("disabled_rw", node4.getFlagName());
+        assertEquals("enabled_fixed_ro", node5.getFlagName());
+        assertEquals("enabled_ro", node6.getFlagName());
+        assertEquals("enabled_fixed_ro", node7.getFlagName());
+        assertEquals("disabled_rw", node8.getFlagName());
+
+        assertEquals(0, node1.getPackageId());
+        assertEquals(0, node2.getPackageId());
+        assertEquals(2, node3.getPackageId());
+        assertEquals(1, node4.getPackageId());
+        assertEquals(1, node5.getPackageId());
+        assertEquals(1, node6.getPackageId());
+        assertEquals(2, node7.getPackageId());
+        assertEquals(0, node8.getPackageId());
+
+        assertEquals(FlagType.ReadOnlyBoolean, node1.getFlagType());
+        assertEquals(FlagType.ReadWriteBoolean, node2.getFlagType());
+        assertEquals(FlagType.ReadWriteBoolean, node3.getFlagType());
+        assertEquals(FlagType.ReadWriteBoolean, node4.getFlagType());
+        assertEquals(FlagType.FixedReadOnlyBoolean, node5.getFlagType());
+        assertEquals(FlagType.ReadOnlyBoolean, node6.getFlagType());
+        assertEquals(FlagType.FixedReadOnlyBoolean, node7.getFlagType());
+        assertEquals(FlagType.ReadWriteBoolean, node8.getFlagType());
+
+        assertEquals(1, node1.getFlagIndex());
+        assertEquals(2, node2.getFlagIndex());
+        assertEquals(1, node3.getFlagIndex());
+        assertEquals(0, node4.getFlagIndex());
+        assertEquals(1, node5.getFlagIndex());
+        assertEquals(2, node6.getFlagIndex());
+        assertEquals(0, node7.getFlagIndex());
+        assertEquals(0, node8.getFlagIndex());
+
+        assertEquals(-1, node1.getNextOffset());
+        assertEquals(151, node2.getNextOffset());
+        assertEquals(-1, node3.getNextOffset());
+        assertEquals(-1, node4.getNextOffset());
+        assertEquals(236, node5.getNextOffset());
+        assertEquals(-1, node6.getNextOffset());
+        assertEquals(-1, node7.getNextOffset());
+        assertEquals(-1, node8.getNextOffset());
+    }
+}
diff --git a/tools/aconfig/aconfig_storage_file/tests/srcs/FlagValueListTest.java b/tools/aconfig/aconfig_storage_file/tests/srcs/FlagValueListTest.java
new file mode 100644
index 0000000..c18590a
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_file/tests/srcs/FlagValueListTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.aconfig.storage.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.aconfig.storage.FileType;
+import android.aconfig.storage.FlagTable;
+import android.aconfig.storage.FlagValueList;
+import android.aconfig.storage.PackageTable;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class FlagValueListTest {
+
+    @Test
+    public void testFlagValueList_rightHeader() throws Exception {
+        FlagValueList flagValueList =
+                FlagValueList.fromBytes(TestDataUtils.getTestFlagValByteBuffer());
+        FlagValueList.Header header = flagValueList.getHeader();
+        assertEquals(1, header.getVersion());
+        assertEquals("mockup", header.getContainer());
+        assertEquals(FileType.FLAG_VAL, header.getFileType());
+        assertEquals(35, header.getFileSize());
+        assertEquals(8, header.getNumFlags());
+        assertEquals(27, header.getBooleanValueOffset());
+    }
+
+    @Test
+    public void testFlagValueList_rightNode() throws Exception {
+        FlagValueList flagValueList =
+                FlagValueList.fromBytes(TestDataUtils.getTestFlagValByteBuffer());
+
+        boolean[] expected = new boolean[] {false, true, true, false, true, true, true, true};
+        assertEquals(expected.length, flagValueList.size());
+
+        for (int i = 0; i < flagValueList.size(); i++) {
+            assertEquals(expected[i], flagValueList.get(i));
+        }
+    }
+
+    @Test
+    public void testFlagValueList_getValue() throws Exception {
+        PackageTable packageTable =
+                PackageTable.fromBytes(TestDataUtils.getTestPackageMapByteBuffer());
+        FlagTable flagTable = FlagTable.fromBytes(TestDataUtils.getTestFlagMapByteBuffer());
+
+        FlagValueList flagValueList =
+                FlagValueList.fromBytes(TestDataUtils.getTestFlagValByteBuffer());
+
+        PackageTable.Node pNode = packageTable.get("com.android.aconfig.storage.test_1");
+        FlagTable.Node fNode = flagTable.get(pNode.getPackageId(), "enabled_rw");
+        assertTrue(flagValueList.get(pNode.getBooleanStartIndex() + fNode.getFlagIndex()));
+
+        pNode = packageTable.get("com.android.aconfig.storage.test_4");
+        fNode = flagTable.get(pNode.getPackageId(), "enabled_fixed_ro");
+        assertTrue(flagValueList.get(pNode.getBooleanStartIndex() + fNode.getFlagIndex()));
+    }
+}
diff --git a/tools/aconfig/aconfig_storage_file/tests/srcs/PackageTableTest.java b/tools/aconfig/aconfig_storage_file/tests/srcs/PackageTableTest.java
new file mode 100644
index 0000000..e7e19d8
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_file/tests/srcs/PackageTableTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.aconfig.storage.test;
+
+import static org.junit.Assert.assertEquals;
+
+import android.aconfig.storage.FileType;
+import android.aconfig.storage.PackageTable;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class PackageTableTest {
+
+    @Test
+    public void testPackageTable_rightHeader() throws Exception {
+        PackageTable packageTable =
+                PackageTable.fromBytes(TestDataUtils.getTestPackageMapByteBuffer());
+        PackageTable.Header header = packageTable.getHeader();
+        assertEquals(1, header.getVersion());
+        assertEquals("mockup", header.getContainer());
+        assertEquals(FileType.PACKAGE_MAP, header.getFileType());
+        assertEquals(209, header.getFileSize());
+        assertEquals(3, header.getNumPackages());
+        assertEquals(31, header.getBucketOffset());
+        assertEquals(59, header.getNodeOffset());
+    }
+
+    @Test
+    public void testPackageTable_rightNode() throws Exception {
+        PackageTable packageTable =
+                PackageTable.fromBytes(TestDataUtils.getTestPackageMapByteBuffer());
+
+        PackageTable.Node node1 = packageTable.get("com.android.aconfig.storage.test_1");
+        PackageTable.Node node2 = packageTable.get("com.android.aconfig.storage.test_2");
+        PackageTable.Node node4 = packageTable.get("com.android.aconfig.storage.test_4");
+
+        assertEquals("com.android.aconfig.storage.test_1", node1.getPackageName());
+        assertEquals("com.android.aconfig.storage.test_2", node2.getPackageName());
+        assertEquals("com.android.aconfig.storage.test_4", node4.getPackageName());
+
+        assertEquals(0, node1.getPackageId());
+        assertEquals(1, node2.getPackageId());
+        assertEquals(2, node4.getPackageId());
+
+        assertEquals(0, node1.getBooleanStartIndex());
+        assertEquals(3, node2.getBooleanStartIndex());
+        assertEquals(6, node4.getBooleanStartIndex());
+
+        assertEquals(159, node1.getNextOffset());
+        assertEquals(-1, node2.getNextOffset());
+        assertEquals(-1, node4.getNextOffset());
+    }
+}
diff --git a/tools/aconfig/aconfig_storage_file/tests/srcs/TestDataUtils.java b/tools/aconfig/aconfig_storage_file/tests/srcs/TestDataUtils.java
new file mode 100644
index 0000000..f35952d
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_file/tests/srcs/TestDataUtils.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.aconfig.storage.test;
+
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+public final class TestDataUtils {
+    private static final String TEST_PACKAGE_MAP_PATH = "package.map";
+    private static final String TEST_FLAG_MAP_PATH = "flag.map";
+    private static final String TEST_FLAG_VAL_PATH = "flag.val";
+    private static final String TEST_FLAG_INFO_PATH = "flag.info";
+
+    private static final String TESTDATA_PATH =
+            "/data/local/tmp/aconfig_storage_file_test_java/testdata/";
+
+    public static ByteBuffer getTestPackageMapByteBuffer() throws Exception {
+        return readFile(TESTDATA_PATH + TEST_PACKAGE_MAP_PATH);
+    }
+
+    public static ByteBuffer getTestFlagMapByteBuffer() throws Exception {
+        return readFile(TESTDATA_PATH + TEST_FLAG_MAP_PATH);
+    }
+
+    public static ByteBuffer getTestFlagValByteBuffer() throws Exception {
+        return readFile(TESTDATA_PATH + TEST_FLAG_VAL_PATH);
+    }
+
+    public static ByteBuffer getTestFlagInfoByteBuffer() throws Exception {
+        return readFile(TESTDATA_PATH + TEST_FLAG_INFO_PATH);
+    }
+
+    private static ByteBuffer readFile(String fileName) throws Exception {
+        InputStream input = new FileInputStream(fileName);
+        return ByteBuffer.wrap(input.readAllBytes());
+    }
+}