[Thread] add Thread Operational Dataset API

Design doc: go/thread-android-api
Android FR: b/235016403

Bug: 262683651
Change-Id: Icbd4ee4150e3fd78df627c2e726c259e7ee50871
diff --git a/thread/tests/unit/Android.bp b/thread/tests/unit/Android.bp
new file mode 100644
index 0000000..1f16ad1
--- /dev/null
+++ b/thread/tests/unit/Android.bp
@@ -0,0 +1,50 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "ThreadNetworkUnitTests",
+    min_sdk_version: "33",
+    sdk_version: "module_current",
+    manifest: "AndroidManifest.xml",
+    test_config: "AndroidTest.xml",
+    srcs: [
+        "src/**/*.java",
+    ],
+    test_suites: [
+        "general-tests",
+    ],
+    static_libs: [
+        "androidx.test.ext.junit",
+        "compatibility-device-util-axt",
+        "ctstestrunner-axt",
+        "framework-connectivity-pre-jarjar",
+        "framework-connectivity-t-pre-jarjar",
+        "guava-android-testlib",
+        "net-tests-utils",
+        "truth-prebuilt",
+    ],
+    libs: [
+        "android.test.base",
+        "android.test.runner",
+    ],
+    // Test coverage system runs on different devices. Need to
+    // compile for all architectures.
+    compile_multilib: "both",
+}
diff --git a/thread/tests/unit/AndroidManifest.xml b/thread/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..ace7c52
--- /dev/null
+++ b/thread/tests/unit/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    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.
+ -->
+
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.net.thread.unittests">
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.net.thread.unittests"
+        android:label="Unit tests for android.net.thread" />
+</manifest>
diff --git a/thread/tests/unit/AndroidTest.xml b/thread/tests/unit/AndroidTest.xml
new file mode 100644
index 0000000..663ff74
--- /dev/null
+++ b/thread/tests/unit/AndroidTest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    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.
+ -->
+
+<configuration description="Config for Thread network unit test cases">
+    <option name="test-tag" value="ThreadNetworkUnitTests" />
+    <option name="test-suite-tag" value="apct" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="test-file-name" value="ThreadNetworkUnitTests.apk" />
+        <option name="check-min-sdk" value="true" />
+        <option name="cleanup-apks" value="true" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.net.thread.unittests" />
+        <!-- Ignores tests introduced by guava-android-testlib -->
+        <option name="exclude-annotation" value="org.junit.Ignore"/>
+    </test>
+</configuration>
diff --git a/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
new file mode 100644
index 0000000..78eb3d0
--- /dev/null
+++ b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
@@ -0,0 +1,202 @@
+/*
+ * 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 android.net.thread;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.net.IpPrefix;
+import android.net.thread.ActiveOperationalDataset.Builder;
+import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
+import android.util.SparseArray;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.primitives.Bytes;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.security.SecureRandom;
+import java.util.Random;
+
+/** Unit tests for {@link ActiveOperationalDataset}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ActiveOperationalDatasetTest {
+    // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new":
+    // Active Timestamp: 1
+    // Channel: 19
+    // Channel Mask: 0x07FFF800
+    // Ext PAN ID: ACC214689BC40BDF
+    // Mesh Local Prefix: fd64:db12:25f4:7e0b::/64
+    // Network Key: F26B3153760F519A63BAFDDFFC80D2AF
+    // Network Name: OpenThread-d9a0
+    // PAN ID: 0xD9A0
+    // PSKc: A245479C836D551B9CA557F7B9D351B4
+    // Security Policy: 672 onrcb
+    private static final byte[] VALID_DATASET =
+            base16().decode(
+                            "0E080000000000010000000300001335060004001FFFE002"
+                                    + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+                                    + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+                                    + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+                                    + "B9D351B40C0402A0FFF8");
+
+    @Mock private Random mockRandom;
+    @Mock private SecureRandom mockSecureRandom;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    private static byte[] addTlv(byte[] dataset, String tlvHex) {
+        return Bytes.concat(dataset, base16().decode(tlvHex));
+    }
+
+    @Test
+    public void fromThreadTlvs_containsUnknownTlvs_unknownTlvsRetained() {
+        byte[] datasetWithUnknownTlvs = addTlv(VALID_DATASET, "AA01FFBB020102");
+
+        ActiveOperationalDataset dataset1 =
+                ActiveOperationalDataset.fromThreadTlvs(datasetWithUnknownTlvs);
+        ActiveOperationalDataset dataset2 =
+                ActiveOperationalDataset.fromThreadTlvs(dataset1.toThreadTlvs());
+
+        SparseArray<byte[]> unknownTlvs = dataset2.getUnknownTlvs();
+        assertThat(unknownTlvs.size()).isEqualTo(2);
+        assertThat(unknownTlvs.get(0xAA)).isEqualTo(new byte[] {(byte) 0xFF});
+        assertThat(unknownTlvs.get(0xBB)).isEqualTo(new byte[] {0x01, 0x02});
+        assertThat(dataset2).isEqualTo(dataset1);
+    }
+
+    @Test
+    public void createRandomDataset_fieldsAreRandomized() {
+        // Always return the max bounded value
+        doAnswer(invocation -> (int) invocation.getArgument(0) - 1)
+                .when(mockRandom)
+                .nextInt(anyInt());
+        doAnswer(
+                        invocation -> {
+                            byte[] output = invocation.getArgument(0);
+                            for (int i = 0; i < output.length; ++i) {
+                                output[i] = (byte) (i + 10);
+                            }
+                            return null;
+                        })
+                .when(mockRandom)
+                .nextBytes(any(byte[].class));
+        doAnswer(
+                        invocation -> {
+                            byte[] output = invocation.getArgument(0);
+                            for (int i = 0; i < output.length; ++i) {
+                                output[i] = (byte) (i + 30);
+                            }
+                            return null;
+                        })
+                .when(mockSecureRandom)
+                .nextBytes(any(byte[].class));
+
+        ActiveOperationalDataset dataset =
+                ActiveOperationalDataset.createRandomDataset(mockRandom, mockSecureRandom);
+
+        assertThat(dataset.getActiveTimestamp())
+                .isEqualTo(new OperationalDatasetTimestamp(1, 0, false));
+        assertThat(dataset.getExtendedPanId())
+                .isEqualTo(new byte[] {10, 11, 12, 13, 14, 15, 16, 17});
+        assertThat(dataset.getMeshLocalPrefix())
+                .isEqualTo(new IpPrefix("fd0b:0c0d:0e0f:1011::/64"));
+        verify(mockRandom, times(2)).nextBytes(any(byte[].class));
+        assertThat(dataset.getPanId()).isEqualTo(0xfffe); // PAN ID <= 0xfffe
+        verify(mockRandom, times(1)).nextInt(eq(0xffff));
+        assertThat(dataset.getChannel()).isEqualTo(26);
+        verify(mockRandom, times(1)).nextInt(eq(16));
+        assertThat(dataset.getChannelPage()).isEqualTo(0);
+        assertThat(dataset.getChannelMask().size()).isEqualTo(1);
+        assertThat(dataset.getPskc())
+                .isEqualTo(
+                        new byte[] {
+                            30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45
+                        });
+        assertThat(dataset.getNetworkKey())
+                .isEqualTo(
+                        new byte[] {
+                            30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45
+                        });
+        verify(mockSecureRandom, times(2)).nextBytes(any(byte[].class));
+        assertThat(dataset.getSecurityPolicy())
+                .isEqualTo(new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}));
+    }
+
+    @Test
+    public void builder_buildWithTooLongTlvs_throwsIllegalState() {
+        Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+        for (int i = 0; i < 10; i++) {
+            builder.addUnknownTlv(i, new byte[20]);
+        }
+
+        assertThrows(IllegalStateException.class, () -> new Builder().build());
+    }
+
+    @Test
+    public void builder_setUnknownTlvs_success() {
+        ActiveOperationalDataset dataset1 = ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET);
+        SparseArray<byte[]> unknownTlvs = new SparseArray<>(2);
+        unknownTlvs.put(0x33, new byte[] {1, 2, 3});
+        unknownTlvs.put(0x44, new byte[] {1, 2, 3, 4});
+
+        ActiveOperationalDataset dataset2 =
+                new ActiveOperationalDataset.Builder(dataset1).setUnknownTlvs(unknownTlvs).build();
+
+        assertThat(dataset1.getUnknownTlvs().size()).isEqualTo(0);
+        assertThat(dataset2.getUnknownTlvs().size()).isEqualTo(2);
+        assertThat(dataset2.getUnknownTlvs().get(0x33)).isEqualTo(new byte[] {1, 2, 3});
+        assertThat(dataset2.getUnknownTlvs().get(0x44)).isEqualTo(new byte[] {1, 2, 3, 4});
+    }
+
+    @Test
+    public void securityPolicy_fromTooShortTlvValue_throwsIllegalArgument() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> SecurityPolicy.fromTlvValue(new byte[] {0x01}));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> SecurityPolicy.fromTlvValue(new byte[] {0x01, 0x02}));
+    }
+
+    @Test
+    public void securityPolicy_toTlvValue_conversionIsLossLess() {
+        SecurityPolicy policy1 = new SecurityPolicy(200, new byte[] {(byte) 0xFF, (byte) 0xF8});
+
+        SecurityPolicy policy2 = SecurityPolicy.fromTlvValue(policy1.toTlvValue());
+
+        assertThat(policy2).isEqualTo(policy1);
+    }
+}
diff --git a/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java b/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java
new file mode 100644
index 0000000..32063fc
--- /dev/null
+++ b/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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 android.net.thread;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link OperationalDatasetTimestamp}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class OperationalDatasetTimestampTest {
+    @Test
+    public void fromTlvValue_invalidTimestamp_throwsIllegalArguments() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> OperationalDatasetTimestamp.fromTlvValue(new byte[7]));
+    }
+
+    @Test
+    public void fromTlvValue_goodValue_success() {
+        OperationalDatasetTimestamp timestamp =
+                OperationalDatasetTimestamp.fromTlvValue(base16().decode("FFEEDDCCBBAA9989"));
+
+        assertThat(timestamp.getSeconds()).isEqualTo(0xFFEEDDCCBBAAL);
+        // 0x9989 is 0x4CC4 << 1 + 1
+        assertThat(timestamp.getTicks()).isEqualTo(0x4CC4);
+        assertThat(timestamp.isAuthoritativeSource()).isTrue();
+    }
+
+    @Test
+    public void toTlvValue_conversionIsLossLess() {
+        OperationalDatasetTimestamp timestamp1 = new OperationalDatasetTimestamp(100L, 10, true);
+
+        OperationalDatasetTimestamp timestamp2 =
+                OperationalDatasetTimestamp.fromTlvValue(timestamp1.toTlvValue());
+
+        assertThat(timestamp2).isEqualTo(timestamp1);
+    }
+}