Add Telephony satellite on-device access control module and tools

Bug: 313773568
Test: atest SatelliteToolsTests TeleServiceTests

Change-Id: I096310b71ec92beffed42321ed4205ac085dcf8c
diff --git a/utils/satellite/tools/Android.bp b/utils/satellite/tools/Android.bp
new file mode 100644
index 0000000..9aacdd9
--- /dev/null
+++ b/utils/satellite/tools/Android.bp
@@ -0,0 +1,71 @@
+// Copyright (C) 2020 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library_host {
+    name: "satellite-s2storage-tools",
+    srcs: [
+        "src/main/java/**/*.java",
+    ],
+    static_libs: [
+        "jcommander",
+        "guava",
+        "satellite-s2storage-rw",
+        "s2storage_tools",
+        "s2-geometry-library-java",
+    ],
+}
+
+// A tool to create a binary satellite S2 file.
+java_binary_host {
+    name: "satellite_createsats2file",
+    main_class: "com.android.telephony.tools.sats2.CreateSatS2File",
+    static_libs: [
+        "satellite-s2storage-tools",
+    ],
+}
+
+// A tool to create a test satellite S2 file.
+java_binary_host {
+    name: "satellite_createsats2file_test",
+    main_class: "com.android.telephony.tools.sats2.CreateTestSatS2File",
+    static_libs: [
+        "satellite-s2storage-tools",
+    ],
+}
+
+// A tool to dump a satellite S2 file as text for debugging.
+java_binary_host {
+    name: "satellite_dumpsats2file",
+    main_class: "com.android.telephony.tools.sats2.DumpSatS2File",
+    static_libs: [
+        "satellite-s2storage-tools",
+    ],
+}
+
+// Tests for CreateSatS2File.
+java_test_host {
+    name: "SatelliteToolsTests",
+    srcs: ["src/test/java/**/*.java"],
+    static_libs: [
+        "junit",
+        "satellite-s2storage-tools",
+        "s2-geometry-library-java",
+        "satellite-s2storage-testutils"
+    ],
+    test_suites: ["general-tests"],
+}
\ No newline at end of file
diff --git a/utils/satellite/tools/TEST_MAPPING b/utils/satellite/tools/TEST_MAPPING
new file mode 100644
index 0000000..df9511a
--- /dev/null
+++ b/utils/satellite/tools/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+    "postsubmit": [
+        {
+            "name": "SatelliteToolsTests"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/CreateSatS2File.java b/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/CreateSatS2File.java
new file mode 100644
index 0000000..f82cd5c
--- /dev/null
+++ b/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/CreateSatS2File.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2023 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 com.android.telephony.tools.sats2;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.ParameterException;
+
+/** Creates a Sat S2 file from the list of S2 cells. */
+public final class CreateSatS2File {
+    /**
+     * Usage:
+     * CreateSatS2File <[input] s2 cells file> <[input] s2 level of input data>
+     *     <[input] whether s2 cells is an allowed list> <[output] sat s2 file>
+     */
+    public static void main(String[] args) throws Exception {
+        Arguments arguments = new Arguments();
+        JCommander.newBuilder()
+                .addObject(arguments)
+                .build()
+                .parse(args);
+        String inputFile = arguments.inputFile;
+        int s2Level = arguments.s2Level;
+        String outputFile = arguments.outputFile;
+        boolean isAllowedList = Arguments.getBooleanValue(arguments.isAllowedList);
+        SatS2FileCreator.create(inputFile, s2Level, isAllowedList, outputFile);
+    }
+
+    private static class Arguments {
+        @Parameter(names = "--input-file",
+                description = "s2 cells file",
+                required = true)
+        public String inputFile;
+
+        @Parameter(names = "--s2-level",
+                description = "s2 level of input data",
+                required = true)
+        public int s2Level;
+
+        @Parameter(names = "--is-allowed-list",
+                description = "whether s2 cells file contains an allowed list of cells",
+                required = true)
+        public String isAllowedList;
+
+        @Parameter(names = "--output-file",
+                description = "sat s2 file",
+                required = true)
+        public String outputFile;
+
+        public static Boolean getBooleanValue(String value) {
+            if ("false".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value)) {
+                return Boolean.parseBoolean(value);
+            } else {
+                throw new ParameterException("Invalid boolean string:" + value);
+            }
+        }
+    }
+}
diff --git a/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/CreateTestSatS2File.java b/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/CreateTestSatS2File.java
new file mode 100644
index 0000000..f9a9347
--- /dev/null
+++ b/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/CreateTestSatS2File.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2023 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 com.android.telephony.tools.sats2;
+
+import com.android.storage.s2.S2LevelRange;
+import com.android.telephony.sats2range.read.SatS2RangeFileFormat;
+import com.android.telephony.sats2range.write.SatS2RangeFileWriter;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+
+/** Creates a Sat S2 file with a small amount of test data. Useful for testing other tools. */
+public final class CreateTestSatS2File {
+
+    /**
+     * Usage:
+     * CreateTestSatS2File &lt;file name&gt;
+     */
+    public static void main(String[] args) throws Exception {
+        File file = new File(args[0]);
+
+        SatS2RangeFileFormat fileFormat = FileFormats.getFileFormatForLevel(12, true);
+        if (fileFormat.getPrefixBitCount() != 11) {
+            throw new IllegalStateException("Fake data requires 11 prefix bits");
+        }
+
+        try (SatS2RangeFileWriter satS2RangeFileWriter =
+                     SatS2RangeFileWriter.open(file, fileFormat)) {
+            // Two ranges that share a prefix.
+            S2LevelRange range1 = new S2LevelRange(
+                    fileFormat.createCellId(0b100_11111111, 1000),
+                    fileFormat.createCellId(0b100_11111111, 2000));
+            S2LevelRange range2 = new S2LevelRange(
+                    fileFormat.createCellId(0b100_11111111, 2000),
+                    fileFormat.createCellId(0b100_11111111, 3000));
+            // This range has a different face, so a different prefix, and will be in a different
+            // suffix table.
+            S2LevelRange range3 = new S2LevelRange(
+                    fileFormat.createCellId(0b101_11111111, 1000),
+                    fileFormat.createCellId(0b101_11111111, 2000));
+            List<S2LevelRange> allRanges = listOf(range1, range2, range3);
+            satS2RangeFileWriter.createSortedSuffixBlocks(allRanges.iterator());
+        }
+    }
+
+    @SafeVarargs
+    private static <E> List<E> listOf(E... values) {
+        return Arrays.asList(values);
+    }
+}
diff --git a/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/DumpSatS2File.java b/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/DumpSatS2File.java
new file mode 100644
index 0000000..2a9ce37
--- /dev/null
+++ b/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/DumpSatS2File.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2023 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 com.android.telephony.tools.sats2;
+
+import com.android.storage.tools.block.DumpBlockFile;
+import com.android.telephony.sats2range.read.SatS2RangeFileReader;
+import com.android.telephony.tools.sats2.dump.SatS2RangeFileDumper;
+
+import java.io.File;
+
+/**
+ * Dumps information about a Sat S2 data file. Like {@link DumpBlockFile} but it knows details about
+ * the Sat S2 format and can provide more detailed information.
+ */
+public final class DumpSatS2File {
+
+    /**
+     * Usage:
+     * DumpSatFile <[input] sat s2 file name> <[output] output directory name>
+     */
+    public static void main(String[] args) throws Exception {
+        String satS2FileName = args[0];
+        String outputDirName = args[1];
+
+        File outputDir = new File(outputDirName);
+        outputDir.mkdirs();
+
+        File satS2File = new File(satS2FileName);
+        try (SatS2RangeFileReader reader = SatS2RangeFileReader.open(satS2File)) {
+            reader.visit(new SatS2RangeFileDumper(outputDir));
+        }
+    }
+}
diff --git a/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/FileFormats.java b/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/FileFormats.java
new file mode 100644
index 0000000..b800897
--- /dev/null
+++ b/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/FileFormats.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 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 com.android.telephony.tools.sats2;
+
+import com.android.telephony.sats2range.read.SatS2RangeFileFormat;
+
+/** Some sample file formats. */
+public final class FileFormats {
+
+    // level 12: 27 S2 cell ID bits split 11 + 16,
+    // suffix table: 24 bits, 16/24 for cell id suffix, 8/24 dedicated to range
+    private static final SatS2RangeFileFormat FILE_FORMAT_12_ALLOWED_LIST =
+            new SatS2RangeFileFormat(12, 11, 16, 1, 24, true);
+    private static final SatS2RangeFileFormat FILE_FORMAT_12_DISALLOWED_LIST =
+            new SatS2RangeFileFormat(12, 11, 16, 1, 24, false);
+
+    // level 14: 31 S2 cell ID bits split 13 + 18,
+    // suffix table: 32 bits, 18/32 for cell id suffix, 14/32 dedicated to range
+    private static final SatS2RangeFileFormat FILE_FORMAT_14_ALLOWED_LIST =
+            new SatS2RangeFileFormat(14, 13, 18, 1, 32, true);
+    private static final SatS2RangeFileFormat FILE_FORMAT_14_DISALLOWED_LIST =
+            new SatS2RangeFileFormat(14, 13, 18, 1, 32, false);
+
+    // level 16: 35 S2 cell ID bits split 13 + 22,
+    // suffix table: 32 bits, 22/32 for cell id suffix, 10/32 dedicated to range
+    private static final SatS2RangeFileFormat FILE_FORMAT_16_ALLOWED_LIST =
+            new SatS2RangeFileFormat(16, 13, 22, 1, 32, true);
+    private static final SatS2RangeFileFormat FILE_FORMAT_16_DISALLOWED_LIST =
+            new SatS2RangeFileFormat(16, 13, 22, 1, 32, false);
+
+    /** Maps an S2 level to one of the file format constants declared on by class. */
+    public static SatS2RangeFileFormat getFileFormatForLevel(int s2Level, boolean isAllowedList) {
+        switch (s2Level) {
+            case 12:
+                return isAllowedList ? FILE_FORMAT_12_ALLOWED_LIST : FILE_FORMAT_12_DISALLOWED_LIST;
+            case 14:
+                return isAllowedList ? FILE_FORMAT_14_ALLOWED_LIST : FILE_FORMAT_14_DISALLOWED_LIST;
+            case 16:
+                return isAllowedList ? FILE_FORMAT_16_ALLOWED_LIST : FILE_FORMAT_16_DISALLOWED_LIST;
+            default:
+                throw new IllegalArgumentException("s2Level=" + s2Level
+                        + ", isAllowedList=" + isAllowedList + " not mapped");
+        }
+    }
+}
diff --git a/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/SatS2FileCreator.java b/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/SatS2FileCreator.java
new file mode 100644
index 0000000..b701a7b
--- /dev/null
+++ b/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/SatS2FileCreator.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2023 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 com.android.telephony.tools.sats2;
+
+import com.android.storage.s2.S2LevelRange;
+import com.android.telephony.sats2range.read.SatS2RangeFileFormat;
+import com.android.telephony.sats2range.read.SatS2RangeFileReader;
+import com.android.telephony.sats2range.write.SatS2RangeFileWriter;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.geometry.S2CellId;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Scanner;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/** A util class for creating a satellite S2 file from the list of S2 cells. */
+public final class SatS2FileCreator {
+    /**
+     * @param inputFile The input text file containing the list of S2 Cell IDs. Each line in the
+     *                  file contains a number in the range of a signed-64bit number which
+     *                  represents the ID of a S2 cell.
+     * @param s2Level The S2 level of all S2 cells in the input file.
+     * @param isAllowedList {@code true} means the input file contains an allowed list of S2 cells.
+     *                      {@code false} means the input file contains a disallowed list of S2
+     *                      cells.
+     * @param outputFile The output file to which the satellite S2 data in block format will be
+     *                   written.
+     */
+    public static void create(String inputFile, int s2Level, boolean isAllowedList,
+            String outputFile) throws Exception {
+        // Read a list of S2 cells from input file
+        List<Long> s2Cells = readS2CellsFromFile(inputFile);
+        System.out.println("Number of S2 cells read from file:" + s2Cells.size());
+
+        // Convert the input list of S2 Cells into the list of sorted S2CellId
+        List<S2CellId> sortedS2CellIds = s2Cells.stream()
+                .map(x -> new S2CellId(x))
+                .collect(Collectors.toList());
+        // IDs of S2CellId are converted to unsigned long numbers, which will be then used to
+        // compare S2CellId.
+        Collections.sort(sortedS2CellIds);
+
+        // Compress the list of S2CellId into S2 ranges
+        List<SatS2Range> satS2Ranges = createSatS2Ranges(sortedS2CellIds, s2Level);
+
+        // Write the S2 ranges into a block file
+        SatS2RangeFileFormat fileFormat =
+                FileFormats.getFileFormatForLevel(s2Level, isAllowedList);
+        try (SatS2RangeFileWriter satS2RangeFileWriter =
+                     SatS2RangeFileWriter.open(new File(outputFile), fileFormat)) {
+            Iterator<S2LevelRange> s2LevelRangeIterator = satS2Ranges
+                    .stream()
+                    .map(x -> new S2LevelRange(x.rangeStart.id(), x.rangeEnd.id()))
+                    .iterator();
+            /*
+             * Group the sorted ranges into contiguous suffix blocks. Big ranges might get split as
+             * needed to fit them into suffix blocks.
+             */
+            satS2RangeFileWriter.createSortedSuffixBlocks(s2LevelRangeIterator);
+        }
+
+        // Validate the output block file
+        System.out.println("Validating the output block file...");
+        try (SatS2RangeFileReader satS2RangeFileReader =
+                     SatS2RangeFileReader.open(new File(outputFile))) {
+            if (isAllowedList != satS2RangeFileReader.isAllowedList()) {
+                throw new IllegalStateException("isAllowedList="
+                        + satS2RangeFileReader.isAllowedList() + " does not match the input "
+                        + "argument=" + isAllowedList);
+            }
+
+            // Verify that all input S2 cells are present in the output block file
+            for (S2CellId s2CellId : sortedS2CellIds) {
+                if (satS2RangeFileReader.findEntryByCellId(s2CellId.id()) == null) {
+                    throw new IllegalStateException("s2CellId=" + s2CellId
+                            + " is not present in the output sat s2 file");
+                }
+            }
+
+            // Verify the cell right before the first cell in the sortedS2CellIds is not present in
+            // the output block file
+            S2CellId prevCell = sortedS2CellIds.get(0).prev();
+            if (!sortedS2CellIds.contains(prevCell)
+                    && satS2RangeFileReader.findEntryByCellId(prevCell.id()) != null) {
+                throw new IllegalStateException("The cell " + prevCell + ", which is right "
+                        + "before the first cell is unexpectedly present in the output sat s2"
+                        + " file");
+            } else {
+                System.out.println("prevCell=" + prevCell + " is in the sortedS2CellIds");
+            }
+
+            // Verify the cell right after the last cell in the sortedS2CellIds is not present in
+            // the output block file
+            S2CellId nextCell = sortedS2CellIds.get(sortedS2CellIds.size() - 1).next();
+            if (!sortedS2CellIds.contains(nextCell)
+                    && satS2RangeFileReader.findEntryByCellId(nextCell.id()) != null) {
+                throw new IllegalStateException("The cell " + nextCell + ", which is right "
+                        + "after the last cell is unexpectedly present in the output sat s2"
+                        + " file");
+            } else {
+                System.out.println("nextCell=" + nextCell + " is in the sortedS2CellIds");
+            }
+        }
+        System.out.println("Successfully validated the output block file");
+    }
+
+    /**
+     * Read a list of S2 cells from the inputFile.
+     *
+     * @param inputFile A file containing the list of S2 cells. Each line in the inputFile contains
+     *                  a long number - the ID of a S2 cell.
+     * @return A list of S2 cells.
+     */
+    private static List<Long> readS2CellsFromFile(String inputFile) throws Exception {
+        List<Long> s2Cells = new ArrayList();
+        InputStream inputStream = new FileInputStream(inputFile);
+        try (Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8.name())) {
+            while (scanner.hasNextLong()) {
+                s2Cells.add(scanner.nextLong());
+            }
+            if (scanner.hasNextLine()) {
+                throw new IllegalStateException("Input s2 cell file has invalid format, "
+                        + "current line=" + scanner.nextLine());
+            }
+        }
+        return s2Cells;
+    }
+
+    /**
+     * Compress the list of sorted S2CellId into S2 ranges.
+     *
+     * @param sortedS2CellIds List of S2CellId sorted in ascending order.
+     * @param s2Level The level of all S2CellId.
+     * @return List of S2 ranges.
+     */
+    private static List<SatS2Range> createSatS2Ranges(List<S2CellId> sortedS2CellIds, int s2Level) {
+        Stopwatch stopwatch = Stopwatch.createStarted();
+        List<SatS2Range> ranges = new ArrayList<>();
+        if (sortedS2CellIds != null && sortedS2CellIds.size() > 0) {
+            S2CellId rangeStart = null;
+            S2CellId rangeEnd = null;
+            for (int i = 0; i < sortedS2CellIds.size(); i++) {
+                S2CellId currentS2CellId = sortedS2CellIds.get(i);
+                checkCellIdIsAtLevel(currentS2CellId, s2Level);
+
+                SatS2Range currentRange = createS2Range(currentS2CellId, s2Level);
+                S2CellId currentS2CellRangeStart = currentRange.rangeStart;
+                S2CellId currentS2CellRangeEnd = currentRange.rangeEnd;
+
+                if (rangeStart == null) {
+                    // First time round the loop initialize rangeStart / rangeEnd only.
+                    rangeStart = currentS2CellRangeStart;
+                } else if (rangeEnd.id() != currentS2CellRangeStart.id()) {
+                    // If there's a gap between cellIds, store the range we have so far and start a
+                    // new range.
+                    ranges.add(new SatS2Range(rangeStart, rangeEnd));
+                    rangeStart = currentS2CellRangeStart;
+                }
+                rangeEnd = currentS2CellRangeEnd;
+            }
+            ranges.add(new SatS2Range(rangeStart, rangeEnd));
+        }
+
+        // Sorting the ranges is not necessary. As the input is sorted , it will already be sorted.
+        System.out.printf("Created %s SatS2Ranges in %s milliseconds\n",
+                ranges.size(), stopwatch.elapsed(TimeUnit.MILLISECONDS));
+        return ranges;
+    }
+
+    /**
+     * @return A pair of S2CellId for the range [s2CellId, s2CellId's next sibling)
+     */
+    private static SatS2Range createS2Range(
+            S2CellId s2CellId, int s2Level) {
+        // Since s2CellId is at s2Level, s2CellId.childBegin(s2Level) returns itself.
+        S2CellId firstS2CellRangeStart = s2CellId.childBegin(s2Level);
+        // Get the immediate next sibling of s2CellId
+        S2CellId firstS2CellRangeEnd = s2CellId.childEnd(s2Level);
+
+        if (firstS2CellRangeEnd.face() < firstS2CellRangeStart.face()
+                || !firstS2CellRangeEnd.isValid()) {
+            // Fix this if it becomes an issue.
+            throw new IllegalStateException("firstS2CellId=" + s2CellId
+                    + ", childEnd(" + s2Level + ") produced an unsupported"
+                    + " value=" + firstS2CellRangeEnd);
+        }
+        return new SatS2Range(firstS2CellRangeStart, firstS2CellRangeEnd);
+    }
+
+    private static void checkCellIdIsAtLevel(S2CellId cellId, int s2Level) {
+        if (cellId.level() != s2Level) {
+            throw new IllegalStateException("Bad level for cellId=" + cellId
+                    + ". Must be s2Level=" + s2Level);
+        }
+    }
+
+    /**
+     * A range of S2 cell IDs at a fixed S2 level. The range is expressed as a start cell ID
+     * (inclusive) and an end cell ID (exclusive).
+     */
+    private static class SatS2Range {
+        public final S2CellId rangeStart;
+        public final S2CellId rangeEnd;
+
+        /**
+         * Creates an instance. If the range is invalid or the cell IDs are from different levels
+         * this method throws an {@link IllegalArgumentException}.
+         */
+        SatS2Range(S2CellId rangeStart, S2CellId rangeEnd) {
+            this.rangeStart = Objects.requireNonNull(rangeStart);
+            this.rangeEnd = Objects.requireNonNull(rangeEnd);
+            if (rangeStart.level() != rangeEnd.level()) {
+                throw new IllegalArgumentException(
+                        "Levels differ: rangeStart=" + rangeStart + ", rangeEnd=" + rangeEnd);
+            }
+            if (rangeStart.greaterOrEquals(rangeEnd)) {
+                throw new IllegalArgumentException(
+                        "Range start (" + rangeStart + " >= range end (" + rangeEnd + ")");
+            }
+        }
+    }
+}
diff --git a/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/dump/HeaderBlockDumper.java b/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/dump/HeaderBlockDumper.java
new file mode 100644
index 0000000..69a3f70
--- /dev/null
+++ b/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/dump/HeaderBlockDumper.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2023 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 com.android.telephony.tools.sats2.dump;
+
+import com.android.storage.tools.block.dump.SingleFileDumper;
+import com.android.telephony.sats2range.read.HeaderBlock;
+import com.android.telephony.sats2range.read.SatS2RangeFileFormat;
+
+import java.io.File;
+
+/** A {@link HeaderBlock.HeaderBlockVisitor} that dumps information to a file. */
+final class HeaderBlockDumper extends SingleFileDumper implements HeaderBlock.HeaderBlockVisitor {
+
+    HeaderBlockDumper(File headerBlockFile) {
+        super(headerBlockFile);
+    }
+
+    @Override
+    public void visitFileFormat(SatS2RangeFileFormat fileFormat) {
+        println("File format");
+        println("===========");
+        println(fileFormat.toString());
+        println();
+    }
+}
+
diff --git a/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/dump/SatS2RangeFileDumper.java b/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/dump/SatS2RangeFileDumper.java
new file mode 100644
index 0000000..307275a
--- /dev/null
+++ b/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/dump/SatS2RangeFileDumper.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2023 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 com.android.telephony.tools.sats2.dump;
+
+import static com.android.storage.tools.block.dump.DumpUtils.binaryStringLength;
+import static com.android.storage.tools.block.dump.DumpUtils.hexStringLength;
+import static com.android.storage.tools.block.dump.DumpUtils.zeroPadBinary;
+import static com.android.storage.tools.block.dump.DumpUtils.zeroPadHex;
+
+import com.android.storage.tools.block.dump.SingleFileDumper;
+import com.android.telephony.sats2range.read.HeaderBlock;
+import com.android.telephony.sats2range.read.SatS2RangeFileFormat;
+import com.android.telephony.sats2range.read.SatS2RangeFileReader;
+import com.android.telephony.sats2range.read.SuffixTableBlock;
+import com.android.telephony.sats2range.read.SuffixTableExtraInfo;
+
+import java.io.File;
+
+/** A {@link SatS2RangeFileReader.SatS2RangeFileVisitor} that dumps information to a file. */
+public final class SatS2RangeFileDumper implements SatS2RangeFileReader.SatS2RangeFileVisitor {
+
+    private final File mOutputDir;
+
+    private int mMaxPrefix;
+
+    private int mMaxPrefixBinaryLength;
+
+    private int mMaxPrefixHexLength;
+
+    private SingleFileDumper mExtraInfoDumper;
+
+    public SatS2RangeFileDumper(File outputDir) {
+        mOutputDir = outputDir;
+    }
+
+    @Override
+    public void begin() throws VisitException {
+        mExtraInfoDumper = new SingleFileDumper(new File(mOutputDir, "suffixtable_extrainfo.txt"));
+        mExtraInfoDumper.begin();
+    }
+
+    @Override
+    public void visitSuffixTableExtraInfo(SuffixTableExtraInfo suffixTableExtraInfo) {
+        int prefix = suffixTableExtraInfo.getPrefix();
+        mExtraInfoDumper.println("prefix=" + zeroPadBinary(mMaxPrefixBinaryLength, prefix)
+                + "(" + zeroPadHex(mMaxPrefixHexLength, prefix) + ")"
+                + ", entryCount=" + suffixTableExtraInfo.getEntryCount());
+    }
+
+    @Override
+    public void visitHeaderBlock(HeaderBlock headerBlock) throws VisitException {
+        File headerFile = new File(mOutputDir, "header.txt");
+        headerBlock.visit(new HeaderBlockDumper(headerFile));
+        SatS2RangeFileFormat fileFormat = headerBlock.getFileFormat();
+        mMaxPrefix = fileFormat.getMaxPrefixValue();
+        mMaxPrefixBinaryLength = binaryStringLength(mMaxPrefix);
+        mMaxPrefixHexLength = hexStringLength(mMaxPrefix);
+    }
+
+    @Override
+    public void visitSuffixTableBlock(SuffixTableBlock suffixTableBlock)
+            throws VisitException {
+        suffixTableBlock.visit(new SuffixTableBlockDumper(mOutputDir, mMaxPrefix));
+    }
+
+    @Override
+    public void end() throws VisitException {
+        mExtraInfoDumper.end();
+    }
+}
+
diff --git a/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/dump/SuffixTableBlockDumper.java b/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/dump/SuffixTableBlockDumper.java
new file mode 100644
index 0000000..a5d75b4
--- /dev/null
+++ b/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/dump/SuffixTableBlockDumper.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2023 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 com.android.telephony.tools.sats2.dump;
+
+import static com.android.storage.tools.block.dump.DumpUtils.binaryStringLength;
+import static com.android.storage.tools.block.dump.DumpUtils.createPrintWriter;
+import static com.android.storage.tools.block.dump.DumpUtils.generateDumpFile;
+import static com.android.storage.tools.block.dump.DumpUtils.hexStringLength;
+import static com.android.storage.tools.block.dump.DumpUtils.zeroPadBinary;
+import static com.android.storage.tools.block.dump.DumpUtils.zeroPadHex;
+
+import com.android.telephony.sats2range.read.SuffixTableBlock;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.util.Objects;
+
+/** A {@link SuffixTableBlock.SuffixTableBlockVisitor} that dumps information to a file. */
+public final class SuffixTableBlockDumper implements SuffixTableBlock.SuffixTableBlockVisitor {
+
+    private final File mOutputDir;
+
+    private final int mMaxPrefix;
+
+    public SuffixTableBlockDumper(File outputDir, int maxPrefix) {
+        mOutputDir = Objects.requireNonNull(outputDir);
+        mMaxPrefix = maxPrefix;
+    }
+
+    @Override
+    public void visit(SuffixTableBlock suffixTableBlock) throws VisitException {
+        int tablePrefix = suffixTableBlock.getPrefix();
+        int prefixHexLength = hexStringLength(tablePrefix);
+        int prefixBinaryLength = binaryStringLength(tablePrefix);
+        File suffixTableFile =
+                generateDumpFile(mOutputDir, "suffixtable_", tablePrefix, mMaxPrefix);
+        try (PrintWriter writer = createPrintWriter(suffixTableFile)) {
+            writer.println("Prefix value=" + zeroPadBinary(prefixBinaryLength, tablePrefix)
+                    + " (" + zeroPadHex(prefixHexLength, tablePrefix) + ")");
+            int entryCount = suffixTableBlock.getEntryCount();
+            writer.println("Entry count=" + entryCount);
+            if (entryCount > 0) {
+                for (int i = 0; i < entryCount; i++) {
+                    writer.println(
+                            "[" + i + "]=" + suffixTableBlock.getEntryByIndex(i)
+                                    .getSuffixTableRange());
+                }
+            }
+        }
+    }
+}
+
diff --git a/utils/satellite/tools/src/test/java/com/android/telephony/tools/sats2/CreateSatS2FileTest.java b/utils/satellite/tools/src/test/java/com/android/telephony/tools/sats2/CreateSatS2FileTest.java
new file mode 100644
index 0000000..80c1807
--- /dev/null
+++ b/utils/satellite/tools/src/test/java/com/android/telephony/tools/sats2/CreateSatS2FileTest.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2023 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 com.android.telephony.tools.sats2;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import com.android.telephony.sats2range.read.SatS2RangeFileFormat;
+import com.android.telephony.sats2range.read.SatS2RangeFileReader;
+import com.android.telephony.sats2range.utils.TestUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+
+/** Tests for {@link CreateSatS2File} */
+public final class CreateSatS2FileTest {
+    private Path mTempDirPath;
+
+    @Before
+    public void setUp() throws IOException {
+        mTempDirPath = TestUtils.createTempDir(this.getClass());
+    }
+
+    @After
+    public void tearDown() throws IOException {
+        if (mTempDirPath != null) {
+            TestUtils.deleteDirectory(mTempDirPath);
+        }
+    }
+
+    @Test
+    public void testCreateSatS2FileWithValidInput_AllowedList() throws Exception {
+        testCreateSatS2FileWithValidInput(true);
+    }
+
+    @Test
+    public void testCreateSatS2FileWithValidInput_DisallowedList() throws Exception {
+        testCreateSatS2FileWithValidInput(false);
+    }
+
+    @Test
+    public void testCreateSatS2FileWithInvalidInput() throws Exception {
+        int s2Level = 12;
+        boolean isAllowedList = true;
+        Path inputDirPath = mTempDirPath.resolve("input");
+        Files.createDirectory(inputDirPath);
+        Path inputFilePath = inputDirPath.resolve("s2cells.txt");
+
+        Path outputDirPath = mTempDirPath.resolve("output");
+        Files.createDirectory(outputDirPath);
+        Path outputFilePath = outputDirPath.resolve("sats2.dat");
+
+        // Create test input S2 cell file
+        SatS2RangeFileFormat fileFormat = FileFormats.getFileFormatForLevel(s2Level, isAllowedList);
+        TestUtils.createInvalidTestS2CellFile(inputFilePath.toFile(), fileFormat);
+
+        // Commandline input arguments
+        String[] args = {
+                "--input-file", inputFilePath.toAbsolutePath().toString(),
+                "--s2-level", String.valueOf(s2Level),
+                "--is-allowed-list", isAllowedList ? "true" : "false",
+                "--output-file", outputFilePath.toAbsolutePath().toString()
+        };
+
+        // Execute the tool CreateSatS2File and expect exception
+        try {
+            CreateSatS2File.main(args);
+        } catch (Exception ex) {
+            // Expected exception
+            return;
+        }
+        fail("Exception should have been caught");
+    }
+
+    private void testCreateSatS2FileWithValidInput(boolean isAllowedList) throws Exception {
+        int s2Level = 12;
+        Path inputDirPath = mTempDirPath.resolve("input");
+        Files.createDirectory(inputDirPath);
+        Path inputFilePath = inputDirPath.resolve("s2cells.txt");
+
+        Path outputDirPath = mTempDirPath.resolve("output");
+        Files.createDirectory(outputDirPath);
+        Path outputFilePath = outputDirPath.resolve("sats2.dat");
+
+        /*
+         * Create test input S2 cell file with the following ranges:
+         * 1) [(prefix=0b100_11111111, suffix=1000), (prefix=0b100_11111111, suffix=2000))
+         * 2) [(prefix=0b100_11111111, suffix=2001), (prefix=0b100_11111111, suffix=3000))
+         * 3) [(prefix=0b101_11111111, suffix=1000), (prefix=0b101_11111111, suffix=2001))
+         */
+        SatS2RangeFileFormat fileFormat = FileFormats.getFileFormatForLevel(s2Level, isAllowedList);
+        TestUtils.createValidTestS2CellFile(inputFilePath.toFile(), fileFormat);
+
+        // Commandline input arguments
+        String[] args = {
+                "--input-file", inputFilePath.toAbsolutePath().toString(),
+                "--s2-level", String.valueOf(s2Level),
+                "--is-allowed-list", isAllowedList ? "true" : "false",
+                "--output-file", outputFilePath.toAbsolutePath().toString()
+        };
+
+        // Execute the tool CreateSatS2File and expect successful result
+        try {
+            CreateSatS2File.main(args);
+        } catch (Exception ex) {
+            fail("Unexpected exception when executing the tool ex=" + ex);
+        }
+
+        // Validate the output block file
+        try {
+            SatS2RangeFileReader satS2RangeFileReader =
+                         SatS2RangeFileReader.open(outputFilePath.toFile());
+            if (isAllowedList != satS2RangeFileReader.isAllowedList()) {
+                fail("isAllowedList="
+                        + satS2RangeFileReader.isAllowedList() + " does not match the input "
+                        + "argument=" + isAllowedList);
+            }
+
+            // Verify an edge cell (prefix=0b100_11111111, suffix=100)
+            long s2CellId = fileFormat.createCellId(0b100_11111111, 100);
+            assertNull(satS2RangeFileReader.findEntryByCellId(s2CellId));
+
+            // Verify a middle cell (prefix=0b100_11111111, suffix=2000)
+            s2CellId = fileFormat.createCellId(0b100_11111111, 2000);
+            assertNull(satS2RangeFileReader.findEntryByCellId(s2CellId));
+
+            // Verify an edge cell (prefix=0b100_11111111, suffix=4000)
+            s2CellId = fileFormat.createCellId(0b100_11111111, 4000);
+            assertNull(satS2RangeFileReader.findEntryByCellId(s2CellId));
+
+            // Verify an edge cell (prefix=0b101_11111111, suffix=500)
+            s2CellId = fileFormat.createCellId(0b101_11111111, 500);
+            assertNull(satS2RangeFileReader.findEntryByCellId(s2CellId));
+
+            // Verify an edge cell (prefix=0b101_11111111, suffix=2001)
+            s2CellId = fileFormat.createCellId(0b101_11111111, 2500);
+            assertNull(satS2RangeFileReader.findEntryByCellId(s2CellId));
+
+            // Verify an edge cell (prefix=0b101_11111111, suffix=2500)
+            s2CellId = fileFormat.createCellId(0b101_11111111, 2500);
+            assertNull(satS2RangeFileReader.findEntryByCellId(s2CellId));
+        } catch (Exception ex) {
+            fail("Unexpected exception when validating the output ex=" + ex);
+        }
+    }
+}