[Thread] add private API ThreadNetworkSpecifier

Bug: 374037595
Change-Id: I8d63a742ec2a03a1cf2f023711696379c975ff80
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkSpecifier.java b/thread/framework/java/android/net/thread/ThreadNetworkSpecifier.java
new file mode 100644
index 0000000..205c16e
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadNetworkSpecifier.java
@@ -0,0 +1,227 @@
+/*
+ * 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.net.thread;
+
+import static android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.NetworkSpecifier;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.net.module.util.HexDump;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Represents and identifies a Thread network.
+ *
+ * @hide
+ */
+public final class ThreadNetworkSpecifier extends NetworkSpecifier implements Parcelable {
+    /** The Extended PAN ID of a Thread network. */
+    @NonNull private final byte[] mExtendedPanId;
+
+    /** The Active Timestamp of a Thread network. */
+    @Nullable private final OperationalDatasetTimestamp mActiveTimestamp;
+
+    private final boolean mRouterEligibleForLeader;
+
+    private ThreadNetworkSpecifier(@NonNull Builder builder) {
+        mExtendedPanId = builder.mExtendedPanId.clone();
+        mActiveTimestamp = builder.mActiveTimestamp;
+        mRouterEligibleForLeader = builder.mRouterEligibleForLeader;
+    }
+
+    /** Returns the Extended PAN ID of the Thread network this specifier refers to. */
+    @NonNull
+    public byte[] getExtendedPanId() {
+        return mExtendedPanId.clone();
+    }
+
+    /**
+     * Returns the Active Timestamp of the Thread network this specifier refers to, or {@code null}
+     * if not specified.
+     */
+    @Nullable
+    public OperationalDatasetTimestamp getActiveTimestamp() {
+        return mActiveTimestamp;
+    }
+
+    /**
+     * Returns {@code true} if this device can be a leader during attachment when there are no
+     * nearby routers.
+     */
+    public boolean isRouterEligibleForLeader() {
+        return mRouterEligibleForLeader;
+    }
+
+    /**
+     * Returns {@code true} if both {@link #getExtendedPanId()} and {@link #getActiveTimestamp()}
+     * (if not {@code null}) of the two {@link ThreadNetworkSpecifier} objects are equal.
+     *
+     * <p>Note value of {@link #isRouterEligibleForLeader()} is expiclitly excluded because this is
+     * not part of the identifier.
+     *
+     * @hide
+     */
+    @Override
+    public boolean canBeSatisfiedBy(@Nullable NetworkSpecifier other) {
+        if (!(other instanceof ThreadNetworkSpecifier)) {
+            return false;
+        }
+        ThreadNetworkSpecifier otherSpecifier = (ThreadNetworkSpecifier) other;
+
+        if (mActiveTimestamp != null && !mActiveTimestamp.equals(otherSpecifier.mActiveTimestamp)) {
+            return false;
+        }
+
+        return Arrays.equals(mExtendedPanId, otherSpecifier.mExtendedPanId);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (!(other instanceof ThreadNetworkSpecifier)) {
+            return false;
+        } else if (this == other) {
+            return true;
+        }
+
+        ThreadNetworkSpecifier otherSpecifier = (ThreadNetworkSpecifier) other;
+
+        return Arrays.equals(mExtendedPanId, otherSpecifier.mExtendedPanId)
+                && Objects.equals(mActiveTimestamp, otherSpecifier.mActiveTimestamp)
+                && mRouterEligibleForLeader == otherSpecifier.mRouterEligibleForLeader;
+    }
+
+    @Override
+    public int hashCode() {
+        return deepHashCode(mExtendedPanId, mActiveTimestamp, mRouterEligibleForLeader);
+    }
+
+    /** An easy-to-use wrapper of {@link Arrays#deepHashCode}. */
+    private static int deepHashCode(Object... values) {
+        return Arrays.deepHashCode(values);
+    }
+
+    @Override
+    public String toString() {
+        return "ThreadNetworkSpecifier{extendedPanId="
+                + HexDump.toHexString(mExtendedPanId)
+                + ", activeTimestamp="
+                + mActiveTimestamp
+                + ", routerEligibleForLeader="
+                + mRouterEligibleForLeader
+                + "}";
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeByteArray(mExtendedPanId);
+        dest.writeByteArray(mActiveTimestamp != null ? mActiveTimestamp.toTlvValue() : null);
+        dest.writeBoolean(mRouterEligibleForLeader);
+    }
+
+    public static final @NonNull Parcelable.Creator<ThreadNetworkSpecifier> CREATOR =
+            new Parcelable.Creator<ThreadNetworkSpecifier>() {
+                @Override
+                public ThreadNetworkSpecifier createFromParcel(Parcel in) {
+                    byte[] extendedPanId = in.createByteArray();
+                    byte[] activeTimestampBytes = in.createByteArray();
+                    OperationalDatasetTimestamp activeTimestamp =
+                            (activeTimestampBytes != null)
+                                    ? OperationalDatasetTimestamp.fromTlvValue(activeTimestampBytes)
+                                    : null;
+                    boolean routerEligibleForLeader = in.readBoolean();
+
+                    return new Builder(extendedPanId)
+                            .setActiveTimestamp(activeTimestamp)
+                            .setRouterEligibleForLeader(routerEligibleForLeader)
+                            .build();
+                }
+
+                @Override
+                public ThreadNetworkSpecifier[] newArray(int size) {
+                    return new ThreadNetworkSpecifier[size];
+                }
+            };
+
+    /** The builder for creating {@link ActiveOperationalDataset} objects. */
+    public static final class Builder {
+        @NonNull private final byte[] mExtendedPanId;
+        @Nullable private OperationalDatasetTimestamp mActiveTimestamp;
+        private boolean mRouterEligibleForLeader;
+
+        /**
+         * Creates a new {@link Builder} object with given Extended PAN ID.
+         *
+         * @throws IllegalArgumentException if {@code extendedPanId} is {@code null} or the length
+         *     is not {@link ActiveOperationalDataset#LENGTH_EXTENDED_PAN_ID}
+         */
+        public Builder(@NonNull byte[] extendedPanId) {
+            if (extendedPanId == null || extendedPanId.length != LENGTH_EXTENDED_PAN_ID) {
+                throw new IllegalArgumentException(
+                        "extendedPanId is null or length is not "
+                                + LENGTH_EXTENDED_PAN_ID
+                                + ": "
+                                + Arrays.toString(extendedPanId));
+            }
+            mExtendedPanId = extendedPanId.clone();
+            mRouterEligibleForLeader = false;
+        }
+
+        /**
+         * Creates a new {@link Builder} object by copying the data in the given {@code specifier}
+         * object.
+         */
+        public Builder(@NonNull ThreadNetworkSpecifier specifier) {
+            this(specifier.getExtendedPanId());
+            setActiveTimestamp(specifier.getActiveTimestamp());
+            setRouterEligibleForLeader(specifier.isRouterEligibleForLeader());
+        }
+
+        /** Sets the Active Timestamp of the Thread network. */
+        @NonNull
+        public Builder setActiveTimestamp(@Nullable OperationalDatasetTimestamp activeTimestamp) {
+            mActiveTimestamp = activeTimestamp;
+            return this;
+        }
+
+        /**
+         * Sets whether this device should be a leader during attachment when there are no nearby
+         * routers.
+         */
+        @NonNull
+        public Builder setRouterEligibleForLeader(boolean eligible) {
+            mRouterEligibleForLeader = eligible;
+            return this;
+        }
+
+        /** Creates a new {@link ThreadNetworkSpecifier} object from values set so far. */
+        @NonNull
+        public ThreadNetworkSpecifier build() {
+            return new ThreadNetworkSpecifier(this);
+        }
+    }
+}
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkSpecifierTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkSpecifierTest.java
new file mode 100644
index 0000000..c83cb7a
--- /dev/null
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkSpecifierTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.net.thread;
+
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.google.common.testing.EqualsTester;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collection;
+
+/** Tests for {@link ThreadNetworkSpecifier}. */
+@SmallTest
+@RunWith(Parameterized.class)
+public final class ThreadNetworkSpecifierTest {
+    public final byte[] mExtendedPanId;
+    public final OperationalDatasetTimestamp mActiveTimestamp;
+    public final boolean mRouterEligibleForLeader;
+
+    @Parameterized.Parameters
+    public static Collection specifierArguments() {
+        var timestampNow = OperationalDatasetTimestamp.fromInstant(Instant.now());
+        return Arrays.asList(
+                new Object[][] {
+                    {new byte[] {0, 1, 2, 3, 4, 5, 6, 7}, null, false},
+                    {new byte[] {1, 1, 1, 1, 2, 2, 2, 2}, timestampNow, true},
+                    {new byte[] {1, 1, 1, 1, 2, 2, 2, 2}, timestampNow, false},
+                });
+    }
+
+    public ThreadNetworkSpecifierTest(
+            byte[] extendedPanId,
+            OperationalDatasetTimestamp activeTimestamp,
+            boolean routerEligibleForLeader) {
+        mExtendedPanId = extendedPanId.clone();
+        mActiveTimestamp = activeTimestamp;
+        mRouterEligibleForLeader = routerEligibleForLeader;
+    }
+
+    @Test
+    public void parcelable_parcelingIsLossLess() {
+        ThreadNetworkSpecifier specifier =
+                new ThreadNetworkSpecifier.Builder(mExtendedPanId)
+                        .setActiveTimestamp(mActiveTimestamp)
+                        .setRouterEligibleForLeader(mRouterEligibleForLeader)
+                        .build();
+        assertParcelingIsLossless(specifier);
+    }
+
+    @Test
+    public void builder_correctValuesAreSet() {
+        ThreadNetworkSpecifier specifier =
+                new ThreadNetworkSpecifier.Builder(mExtendedPanId)
+                        .setActiveTimestamp(mActiveTimestamp)
+                        .setRouterEligibleForLeader(mRouterEligibleForLeader)
+                        .build();
+
+        assertThat(specifier.getExtendedPanId()).isEqualTo(mExtendedPanId);
+        assertThat(specifier.getActiveTimestamp()).isEqualTo(mActiveTimestamp);
+        assertThat(specifier.isRouterEligibleForLeader()).isEqualTo(mRouterEligibleForLeader);
+    }
+
+    @Test
+    public void builderConstructor_specifiersAreEqual() {
+        ThreadNetworkSpecifier specifier1 =
+                new ThreadNetworkSpecifier.Builder(mExtendedPanId)
+                        .setActiveTimestamp(mActiveTimestamp)
+                        .setRouterEligibleForLeader(mRouterEligibleForLeader)
+                        .build();
+
+        ThreadNetworkSpecifier specifier2 = new ThreadNetworkSpecifier.Builder(specifier1).build();
+
+        assertThat(specifier1).isEqualTo(specifier2);
+    }
+
+    @Test
+    public void equalsTester() {
+        var timestampNow = OperationalDatasetTimestamp.fromInstant(Instant.now());
+        new EqualsTester()
+                .addEqualityGroup(
+                        new ThreadNetworkSpecifier.Builder(new byte[] {0, 1, 2, 3, 4, 5, 6, 7})
+                                .setActiveTimestamp(timestampNow)
+                                .setRouterEligibleForLeader(true)
+                                .build(),
+                        new ThreadNetworkSpecifier.Builder(new byte[] {0, 1, 2, 3, 4, 5, 6, 7})
+                                .setActiveTimestamp(timestampNow)
+                                .setRouterEligibleForLeader(true)
+                                .build())
+                .addEqualityGroup(
+                        new ThreadNetworkSpecifier.Builder(new byte[] {0, 1, 2, 3, 4, 5, 6, 7})
+                                .setActiveTimestamp(null)
+                                .setRouterEligibleForLeader(false)
+                                .build(),
+                        new ThreadNetworkSpecifier.Builder(new byte[] {0, 1, 2, 3, 4, 5, 6, 7})
+                                .setActiveTimestamp(null)
+                                .setRouterEligibleForLeader(false)
+                                .build())
+                .addEqualityGroup(
+                        new ThreadNetworkSpecifier.Builder(new byte[] {1, 1, 1, 1, 2, 2, 2, 2})
+                                .setActiveTimestamp(null)
+                                .setRouterEligibleForLeader(false)
+                                .build(),
+                        new ThreadNetworkSpecifier.Builder(new byte[] {1, 1, 1, 1, 2, 2, 2, 2})
+                                .setActiveTimestamp(null)
+                                .setRouterEligibleForLeader(false)
+                                .build())
+                .testEquals();
+    }
+}