Skip to content
Snippets Groups Projects
Commit 898ed693 authored by Kangping Dong's avatar Kangping Dong Committed by Gerrit Code Review
Browse files

Merge "[Thread] add Thread Operational Dataset API" into main

parents 4e475336 7fea5fa6
No related branches found
No related tags found
No related merge requests found
Showing
with 3214 additions and 0 deletions
...@@ -417,6 +417,81 @@ package android.net.nsd { ...@@ -417,6 +417,81 @@ package android.net.nsd {
package android.net.thread { package android.net.thread {
@FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ActiveOperationalDataset implements android.os.Parcelable {
method @NonNull public static android.net.thread.ActiveOperationalDataset createRandomDataset();
method public int describeContents();
method @NonNull public static android.net.thread.ActiveOperationalDataset fromThreadTlvs(@NonNull byte[]);
method @NonNull public android.net.thread.OperationalDatasetTimestamp getActiveTimestamp();
method @IntRange(from=0, to=65535) public int getChannel();
method @NonNull @Size(min=1) public android.util.SparseArray<byte[]> getChannelMask();
method @IntRange(from=0, to=255) public int getChannelPage();
method @NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID) public byte[] getExtendedPanId();
method @NonNull public android.net.IpPrefix getMeshLocalPrefix();
method @NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_NETWORK_KEY) public byte[] getNetworkKey();
method @NonNull @Size(min=android.net.thread.ActiveOperationalDataset.LENGTH_MIN_NETWORK_NAME_BYTES, max=android.net.thread.ActiveOperationalDataset.LENGTH_MAX_NETWORK_NAME_BYTES) public String getNetworkName();
method @IntRange(from=0, to=65534) public int getPanId();
method @NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_PSKC) public byte[] getPskc();
method @NonNull public android.net.thread.ActiveOperationalDataset.SecurityPolicy getSecurityPolicy();
method @NonNull public byte[] toThreadTlvs();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field public static final int CHANNEL_MAX_24_GHZ = 26; // 0x1a
field public static final int CHANNEL_MIN_24_GHZ = 11; // 0xb
field public static final int CHANNEL_PAGE_24_GHZ = 0; // 0x0
field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.ActiveOperationalDataset> CREATOR;
field public static final int LENGTH_EXTENDED_PAN_ID = 8; // 0x8
field public static final int LENGTH_MAX_DATASET_TLVS = 254; // 0xfe
field public static final int LENGTH_MAX_NETWORK_NAME_BYTES = 16; // 0x10
field public static final int LENGTH_MESH_LOCAL_PREFIX_BITS = 64; // 0x40
field public static final int LENGTH_MIN_NETWORK_NAME_BYTES = 1; // 0x1
field public static final int LENGTH_NETWORK_KEY = 16; // 0x10
field public static final int LENGTH_PSKC = 16; // 0x10
}
public static final class ActiveOperationalDataset.Builder {
ctor public ActiveOperationalDataset.Builder(@NonNull android.net.thread.ActiveOperationalDataset);
ctor public ActiveOperationalDataset.Builder();
method @NonNull public android.net.thread.ActiveOperationalDataset build();
method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setActiveTimestamp(@NonNull android.net.thread.OperationalDatasetTimestamp);
method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setChannel(@IntRange(from=0, to=255) int, @IntRange(from=0, to=65535) int);
method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setChannelMask(@NonNull @Size(min=1) android.util.SparseArray<byte[]>);
method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setExtendedPanId(@NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID) byte[]);
method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setMeshLocalPrefix(@NonNull android.net.IpPrefix);
method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setNetworkKey(@NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_NETWORK_KEY) byte[]);
method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setNetworkName(@NonNull @Size(min=android.net.thread.ActiveOperationalDataset.LENGTH_MIN_NETWORK_NAME_BYTES, max=android.net.thread.ActiveOperationalDataset.LENGTH_MAX_NETWORK_NAME_BYTES) String);
method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setPanId(@IntRange(from=0, to=65534) int);
method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setPskc(@NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_PSKC) byte[]);
method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setSecurityPolicy(@NonNull android.net.thread.ActiveOperationalDataset.SecurityPolicy);
}
public static final class ActiveOperationalDataset.SecurityPolicy {
ctor public ActiveOperationalDataset.SecurityPolicy(@IntRange(from=1, to=65535) int, @NonNull @Size(min=android.net.thread.ActiveOperationalDataset.SecurityPolicy.LENGTH_MIN_SECURITY_POLICY_FLAGS) byte[]);
method @NonNull @Size(min=android.net.thread.ActiveOperationalDataset.SecurityPolicy.LENGTH_MIN_SECURITY_POLICY_FLAGS) public byte[] getFlags();
method @IntRange(from=1, to=65535) public int getRotationTimeHours();
field public static final int DEFAULT_ROTATION_TIME_HOURS = 672; // 0x2a0
field public static final int LENGTH_MIN_SECURITY_POLICY_FLAGS = 1; // 0x1
}
@FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class OperationalDatasetTimestamp {
ctor public OperationalDatasetTimestamp(@IntRange(from=0, to=281474976710655L) long, @IntRange(from=0, to=32767) int, boolean);
method @NonNull public static android.net.thread.OperationalDatasetTimestamp fromInstant(@NonNull java.time.Instant);
method @IntRange(from=0, to=281474976710655L) public long getSeconds();
method @IntRange(from=0, to=32767) public int getTicks();
method public boolean isAuthoritativeSource();
method @NonNull public java.time.Instant toInstant();
}
@FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class PendingOperationalDataset implements android.os.Parcelable {
ctor public PendingOperationalDataset(@NonNull android.net.thread.ActiveOperationalDataset, @NonNull android.net.thread.OperationalDatasetTimestamp, @NonNull java.time.Duration);
method public int describeContents();
method @NonNull public static android.net.thread.PendingOperationalDataset fromThreadTlvs(@NonNull byte[]);
method @NonNull public android.net.thread.ActiveOperationalDataset getActiveOperationalDataset();
method @NonNull public java.time.Duration getDelayTimer();
method @NonNull public android.net.thread.OperationalDatasetTimestamp getPendingTimestamp();
method @NonNull public byte[] toThreadTlvs();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.PendingOperationalDataset> CREATOR;
}
@FlaggedApi("com.android.net.thread.flags.thread_enabled") public class ThreadNetworkController { @FlaggedApi("com.android.net.thread.flags.thread_enabled") public class ThreadNetworkController {
method public int getThreadVersion(); method public int getThreadVersion();
field public static final int THREAD_VERSION_1_3 = 4; // 0x4 field public static final int THREAD_VERSION_1_3 = 4; // 0x4
......
/*
* Copyright 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;
parcelable ActiveOperationalDataset;
/*
* 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 android.net.thread.ActiveOperationalDataset.SecurityPolicy.DEFAULT_ROTATION_TIME_HOURS;
import static com.android.internal.util.Preconditions.checkArgument;
import static com.android.internal.util.Preconditions.checkState;
import static com.android.net.module.util.HexDump.dumpHexString;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import android.annotation.FlaggedApi;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.Size;
import android.annotation.SystemApi;
import android.net.IpPrefix;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
import java.io.ByteArrayOutputStream;
import java.net.Inet6Address;
import java.net.UnknownHostException;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.Arrays;
import java.util.Random;
/**
* Data interface for managing a Thread Active Operational Dataset.
*
* <p>An example usage of creating an Active Operational Dataset with random parameters:
*
* <pre>{@code
* ActiveOperationalDataset activeDataset = ActiveOperationalDataset.createRandomDataset();
* }</pre>
*
* <p>or random Dataset with customized Network Name:
*
* <pre>{@code
* ActiveOperationalDataset activeDataset =
* new ActiveOperationalDataset.Builder(ActiveOperationalDataset.createRandomDataset())
* .setNetworkName("MyThreadNet").build();
* }</pre>
*
* <p>If the Active Operational Dataset is already known as <a
* href="https://www.threadgroup.org">Thread TLVs</a>, you can simply use:
*
* <pre>{@code
* ActiveOperationalDataset activeDataset = ActiveOperationalDataset.fromThreadTlvs(datasetTlvs);
* }</pre>
*
* @hide
*/
@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
@SystemApi
public final class ActiveOperationalDataset implements Parcelable {
/** The maximum length of the Active Operational Dataset TLV array in bytes. */
public static final int LENGTH_MAX_DATASET_TLVS = 254;
/** The length of Extended PAN ID in bytes. */
public static final int LENGTH_EXTENDED_PAN_ID = 8;
/** The minimum length of Network Name as UTF-8 bytes. */
public static final int LENGTH_MIN_NETWORK_NAME_BYTES = 1;
/** The maximum length of Network Name as UTF-8 bytes. */
public static final int LENGTH_MAX_NETWORK_NAME_BYTES = 16;
/** The length of Network Key in bytes. */
public static final int LENGTH_NETWORK_KEY = 16;
/** The length of Mesh-Local Prefix in bits. */
public static final int LENGTH_MESH_LOCAL_PREFIX_BITS = 64;
/** The length of PSKc in bytes. */
public static final int LENGTH_PSKC = 16;
/** The 2.4 GHz channel page. */
public static final int CHANNEL_PAGE_24_GHZ = 0;
/** The minimum 2.4GHz channel. */
public static final int CHANNEL_MIN_24_GHZ = 11;
/** The maximum 2.4GHz channel. */
public static final int CHANNEL_MAX_24_GHZ = 26;
/** @hide */
@VisibleForTesting public static final int TYPE_CHANNEL = 0;
/** @hide */
@VisibleForTesting public static final int TYPE_PAN_ID = 1;
/** @hide */
@VisibleForTesting public static final int TYPE_EXTENDED_PAN_ID = 2;
/** @hide */
@VisibleForTesting public static final int TYPE_NETWORK_NAME = 3;
/** @hide */
@VisibleForTesting public static final int TYPE_PSKC = 4;
/** @hide */
@VisibleForTesting public static final int TYPE_NETWORK_KEY = 5;
/** @hide */
@VisibleForTesting public static final int TYPE_MESH_LOCAL_PREFIX = 7;
/** @hide */
@VisibleForTesting public static final int TYPE_SECURITY_POLICY = 12;
/** @hide */
@VisibleForTesting public static final int TYPE_ACTIVE_TIMESTAMP = 14;
/** @hide */
@VisibleForTesting public static final int TYPE_CHANNEL_MASK = 53;
private static final byte MESH_LOCAL_PREFIX_FIRST_BYTE = (byte) 0xfd;
private static final int LENGTH_CHANNEL = 3;
private static final int LENGTH_PAN_ID = 2;
@NonNull
public static final Creator<ActiveOperationalDataset> CREATOR =
new Creator<>() {
@Override
public ActiveOperationalDataset createFromParcel(Parcel in) {
return ActiveOperationalDataset.fromThreadTlvs(in.createByteArray());
}
@Override
public ActiveOperationalDataset[] newArray(int size) {
return new ActiveOperationalDataset[size];
}
};
private final OperationalDatasetTimestamp mActiveTimestamp;
private final String mNetworkName;
private final byte[] mExtendedPanId;
private final int mPanId;
private final int mChannel;
private final int mChannelPage;
private final SparseArray<byte[]> mChannelMask;
private final byte[] mPskc;
private final byte[] mNetworkKey;
private final IpPrefix mMeshLocalPrefix;
private final SecurityPolicy mSecurityPolicy;
private final SparseArray<byte[]> mUnknownTlvs;
private ActiveOperationalDataset(Builder builder) {
this(
requireNonNull(builder.mActiveTimestamp),
requireNonNull(builder.mNetworkName),
requireNonNull(builder.mExtendedPanId),
requireNonNull(builder.mPanId),
requireNonNull(builder.mChannelPage),
requireNonNull(builder.mChannel),
requireNonNull(builder.mChannelMask),
requireNonNull(builder.mPskc),
requireNonNull(builder.mNetworkKey),
requireNonNull(builder.mMeshLocalPrefix),
requireNonNull(builder.mSecurityPolicy),
requireNonNull(builder.mUnknownTlvs));
}
private ActiveOperationalDataset(
OperationalDatasetTimestamp activeTimestamp,
String networkName,
byte[] extendedPanId,
int panId,
int channelPage,
int channel,
SparseArray<byte[]> channelMask,
byte[] pskc,
byte[] networkKey,
IpPrefix meshLocalPrefix,
SecurityPolicy securityPolicy,
SparseArray<byte[]> unknownTlvs) {
this.mActiveTimestamp = activeTimestamp;
this.mNetworkName = networkName;
this.mExtendedPanId = extendedPanId.clone();
this.mPanId = panId;
this.mChannel = channel;
this.mChannelPage = channelPage;
this.mChannelMask = deepCloneSparseArray(channelMask);
this.mPskc = pskc.clone();
this.mNetworkKey = networkKey.clone();
this.mMeshLocalPrefix = meshLocalPrefix;
this.mSecurityPolicy = securityPolicy;
this.mUnknownTlvs = deepCloneSparseArray(unknownTlvs);
}
/**
* Creates a new {@link ActiveOperationalDataset} object from a series of Thread TLVs.
*
* <p>{@code tlvs} can be obtained from the value of a Thread Active Operational Dataset TLV
* (see the <a href="https://www.threadgroup.org/support#specifications">Thread
* specification</a> for the definition) or the return value of {@link #toThreadTlvs}.
*
* @param tlvs a series of Thread TLVs which contain the Active Operational Dataset
* @return the decoded Active Operational Dataset
* @throws IllegalArgumentException if {@code tlvs} is malformed or the length is larger than
* {@link LENGTH_MAX_DATASET_TLVS}
*/
@NonNull
public static ActiveOperationalDataset fromThreadTlvs(@NonNull byte[] tlvs) {
requireNonNull(tlvs, "tlvs cannot be null");
if (tlvs.length > LENGTH_MAX_DATASET_TLVS) {
throw new IllegalArgumentException(
String.format(
"tlvs length exceeds max length %d (actual is %d)",
LENGTH_MAX_DATASET_TLVS, tlvs.length));
}
Builder builder = new Builder();
int i = 0;
while (i < tlvs.length) {
int type = tlvs[i++] & 0xff;
if (i >= tlvs.length) {
throw new IllegalArgumentException(
String.format(
"Found TLV type %d at end of operational dataset with length %d",
type, tlvs.length));
}
int length = tlvs[i++] & 0xff;
if (i + length > tlvs.length) {
throw new IllegalArgumentException(
String.format(
"Found TLV type %d with length %d which exceeds the remaining data"
+ " in the operational dataset with length %d",
type, length, tlvs.length));
}
initWithTlv(builder, type, Arrays.copyOfRange(tlvs, i, i + length));
i += length;
}
try {
return builder.build();
} catch (IllegalStateException e) {
throw new IllegalArgumentException(
"Failed to build the ActiveOperationalDataset object", e);
}
}
private static void initWithTlv(Builder builder, int type, byte[] value) {
// The max length of the dataset is 254 bytes, so the max length of a single TLV value is
// 252 (254 - 1 - 1)
if (value.length > LENGTH_MAX_DATASET_TLVS - 2) {
throw new IllegalArgumentException(
String.format(
"Length of TLV %d exceeds %d (actualLength = %d)",
(type & 0xff), LENGTH_MAX_DATASET_TLVS - 2, value.length));
}
switch (type) {
case TYPE_CHANNEL:
checkArgument(
value.length == LENGTH_CHANNEL,
"Invalid channel (length = %d, expectedLength = %d)",
value.length,
LENGTH_CHANNEL);
builder.setChannel((value[0] & 0xff), ((value[1] & 0xff) << 8) | (value[2] & 0xff));
break;
case TYPE_PAN_ID:
checkArgument(
value.length == LENGTH_PAN_ID,
"Invalid PAN ID (length = %d, expectedLength = %d)",
value.length,
LENGTH_PAN_ID);
builder.setPanId(((value[0] & 0xff) << 8) | (value[1] & 0xff));
break;
case TYPE_EXTENDED_PAN_ID:
builder.setExtendedPanId(value);
break;
case TYPE_NETWORK_NAME:
builder.setNetworkName(new String(value, UTF_8));
break;
case TYPE_PSKC:
builder.setPskc(value);
break;
case TYPE_NETWORK_KEY:
builder.setNetworkKey(value);
break;
case TYPE_MESH_LOCAL_PREFIX:
builder.setMeshLocalPrefix(value);
break;
case TYPE_SECURITY_POLICY:
builder.setSecurityPolicy(SecurityPolicy.fromTlvValue(value));
break;
case TYPE_ACTIVE_TIMESTAMP:
builder.setActiveTimestamp(OperationalDatasetTimestamp.fromTlvValue(value));
break;
case TYPE_CHANNEL_MASK:
builder.setChannelMask(decodeChannelMask(value));
break;
default:
builder.addUnknownTlv(type & 0xff, value);
break;
}
}
private static SparseArray<byte[]> decodeChannelMask(byte[] tlvValue) {
SparseArray<byte[]> channelMask = new SparseArray<>();
int i = 0;
while (i < tlvValue.length) {
int channelPage = tlvValue[i++] & 0xff;
if (i >= tlvValue.length) {
throw new IllegalArgumentException(
"Invalid channel mask - channel mask length is missing");
}
int maskLength = tlvValue[i++] & 0xff;
if (i + maskLength > tlvValue.length) {
throw new IllegalArgumentException(
String.format(
"Invalid channel mask - channel mask is incomplete "
+ "(offset = %d, length = %d, totalLength = %d)",
i, maskLength, tlvValue.length));
}
channelMask.put(channelPage, Arrays.copyOfRange(tlvValue, i, i + maskLength));
i += maskLength;
}
return channelMask;
}
private static void encodeChannelMask(
SparseArray<byte[]> channelMask, ByteArrayOutputStream outputStream) {
ByteArrayOutputStream entryStream = new ByteArrayOutputStream();
for (int i = 0; i < channelMask.size(); i++) {
int key = channelMask.keyAt(i);
byte[] value = channelMask.get(key);
entryStream.write(key);
entryStream.write(value.length);
entryStream.write(value, 0, value.length);
}
byte[] entries = entryStream.toByteArray();
outputStream.write(TYPE_CHANNEL_MASK);
outputStream.write(entries.length);
outputStream.write(entries, 0, entries.length);
}
/**
* Creates a new {@link ActiveOperationalDataset} object with randomized or default parameters.
*
* <p>The randomized (or default) value for each parameter:
*
* <ul>
* <li>{@code Active Timestamp} defaults to {@code new OperationalDatasetTimestamp(1, 0,
* false)}
* <li>{@code Network Name} defaults to "THREAD-PAN-<PAN ID decimal>", for example
* "THREAD-PAN-12345"
* <li>{@code Extended PAN ID} filled with randomly generated bytes
* <li>{@code PAN ID} randomly generated integer in range of [0, 0xfffe]
* <li>{@code Channel Page} defaults to {@link #CHANNEL_PAGE_24_GHZ}
* <li>{@code Channel} randomly selected channel in range of [{@link #CHANNEL_MIN_24_GHZ},
* {@link #CHANNEL_MAX_24_GHZ}]
* <li>{@code Channel Mask} all bits from {@link #CHANNEL_MIN_24_GHZ} to {@link
* #CHANNEL_MAX_24_GHZ} are set to {@code true}
* <li>{@code PSKc} filled with bytes generated by secure random generator
* <li>{@code Network Key} filled with bytes generated by secure random generator
* <li>{@code Mesh-local Prefix} filled with randomly generated bytes except that the first
* byte is always set to {@code 0xfd}
* <li>{@code Security Policy} defaults to {@code new SecurityPolicy(
* DEFAULT_ROTATION_TIME_HOURS, new byte[]{(byte)0xff, (byte)0xf8})}. This is the default
* values required by the Thread 1.2 specification
* </ul>
*
* <p>This method is the recommended way to create a randomized operational dataset for a new
* Thread network. It may be desired to change one or more of the generated value(s). For
* example, to use a more meaningful Network Name. To do that, create a new {@link Builder}
* object from this dataset with {@link Builder#Builder(ActiveOperationalDataset)} and override
* the value with the setters of {@link Builder}.
*
* <p>Note that it's highly discouraged to change the randomly generated Extended PAN ID,
* Network Key or PSKc, as it will compromise the security of a Thread network.
*/
@NonNull
public static ActiveOperationalDataset createRandomDataset() {
return createRandomDataset(new Random(Instant.now().toEpochMilli()), new SecureRandom());
}
/** @hide */
@VisibleForTesting
public static ActiveOperationalDataset createRandomDataset(
Random random, SecureRandom secureRandom) {
int panId = random.nextInt(/* bound= */ 0xffff);
byte[] meshLocalPrefix = newRandomBytes(random, LENGTH_MESH_LOCAL_PREFIX_BITS / 8);
meshLocalPrefix[0] = MESH_LOCAL_PREFIX_FIRST_BYTE;
SparseArray<byte[]> channelMask = new SparseArray<>(1);
channelMask.put(CHANNEL_PAGE_24_GHZ, new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0});
return new Builder()
.setActiveTimestamp(
new OperationalDatasetTimestamp(
/* seconds= */ 1,
/* ticks= */ 0,
/* isAuthoritativeSource= */ false))
.setExtendedPanId(newRandomBytes(random, LENGTH_EXTENDED_PAN_ID))
.setPanId(panId)
.setNetworkName("THREAD-PAN-" + panId)
.setChannel(
CHANNEL_PAGE_24_GHZ,
random.nextInt(CHANNEL_MAX_24_GHZ - CHANNEL_MIN_24_GHZ + 1)
+ CHANNEL_MIN_24_GHZ)
.setChannelMask(channelMask)
.setPskc(newRandomBytes(secureRandom, LENGTH_PSKC))
.setNetworkKey(newRandomBytes(secureRandom, LENGTH_NETWORK_KEY))
.setMeshLocalPrefix(meshLocalPrefix)
.setSecurityPolicy(
new SecurityPolicy(
DEFAULT_ROTATION_TIME_HOURS, new byte[] {(byte) 0xff, (byte) 0xf8}))
.build();
}
private static byte[] newRandomBytes(Random random, int length) {
byte[] result = new byte[length];
random.nextBytes(result);
return result;
}
private static boolean areByteSparseArraysEqual(
@NonNull SparseArray<byte[]> first, @NonNull SparseArray<byte[]> second) {
if (first == second) {
return true;
} else if (first == null || second == null) {
return false;
} else if (first.size() != second.size()) {
return false;
} else {
for (int i = 0; i < first.size(); i++) {
int firstKey = first.keyAt(i);
int secondKey = second.keyAt(i);
if (firstKey != secondKey) {
return false;
}
byte[] firstValue = first.valueAt(i);
byte[] secondValue = second.valueAt(i);
if (!Arrays.equals(firstValue, secondValue)) {
return false;
}
}
return true;
}
}
/** An easy-to-use wrapper of {@link Arrays#deepHashCode}. */
private static int deepHashCode(Object... values) {
return Arrays.deepHashCode(values);
}
/**
* Converts this {@link ActiveOperationalDataset} object to a series of Thread TLVs.
*
* <p>See the <a href="https://www.threadgroup.org/support#specifications">Thread
* specification</a> for the definition of the Thread TLV format.
*
* @return a series of Thread TLVs which contain this Active Operational Dataset
*/
@NonNull
public byte[] toThreadTlvs() {
ByteArrayOutputStream dataset = new ByteArrayOutputStream();
dataset.write(TYPE_ACTIVE_TIMESTAMP);
byte[] activeTimestampBytes = mActiveTimestamp.toTlvValue();
dataset.write(activeTimestampBytes.length);
dataset.write(activeTimestampBytes, 0, activeTimestampBytes.length);
dataset.write(TYPE_NETWORK_NAME);
byte[] networkNameBytes = mNetworkName.getBytes(UTF_8);
dataset.write(networkNameBytes.length);
dataset.write(networkNameBytes, 0, networkNameBytes.length);
dataset.write(TYPE_EXTENDED_PAN_ID);
dataset.write(mExtendedPanId.length);
dataset.write(mExtendedPanId, 0, mExtendedPanId.length);
dataset.write(TYPE_PAN_ID);
dataset.write(LENGTH_PAN_ID);
dataset.write(mPanId >> 8);
dataset.write(mPanId);
dataset.write(TYPE_CHANNEL);
dataset.write(LENGTH_CHANNEL);
dataset.write(mChannelPage);
dataset.write(mChannel >> 8);
dataset.write(mChannel);
encodeChannelMask(mChannelMask, dataset);
dataset.write(TYPE_PSKC);
dataset.write(mPskc.length);
dataset.write(mPskc, 0, mPskc.length);
dataset.write(TYPE_NETWORK_KEY);
dataset.write(mNetworkKey.length);
dataset.write(mNetworkKey, 0, mNetworkKey.length);
dataset.write(TYPE_MESH_LOCAL_PREFIX);
dataset.write(mMeshLocalPrefix.getPrefixLength() / 8);
dataset.write(mMeshLocalPrefix.getRawAddress(), 0, mMeshLocalPrefix.getPrefixLength() / 8);
dataset.write(TYPE_SECURITY_POLICY);
byte[] securityPolicyBytes = mSecurityPolicy.toTlvValue();
dataset.write(securityPolicyBytes.length);
dataset.write(securityPolicyBytes, 0, securityPolicyBytes.length);
for (int i = 0; i < mUnknownTlvs.size(); i++) {
byte[] value = mUnknownTlvs.valueAt(i);
dataset.write(mUnknownTlvs.keyAt(i));
dataset.write(value.length);
dataset.write(value, 0, value.length);
}
return dataset.toByteArray();
}
/** Returns the Active Timestamp. */
@NonNull
public OperationalDatasetTimestamp getActiveTimestamp() {
return mActiveTimestamp;
}
/** Returns the Network Name. */
@NonNull
@Size(min = LENGTH_MIN_NETWORK_NAME_BYTES, max = LENGTH_MAX_NETWORK_NAME_BYTES)
public String getNetworkName() {
return mNetworkName;
}
/** Returns the Extended PAN ID. */
@NonNull
@Size(LENGTH_EXTENDED_PAN_ID)
public byte[] getExtendedPanId() {
return mExtendedPanId.clone();
}
/** Returns the PAN ID. */
@IntRange(from = 0, to = 0xfffe)
public int getPanId() {
return mPanId;
}
/** Returns the Channel. */
@IntRange(from = 0, to = 65535)
public int getChannel() {
return mChannel;
}
/** Returns the Channel Page. */
@IntRange(from = 0, to = 255)
public int getChannelPage() {
return mChannelPage;
}
/**
* Returns the Channel masks. For the returned {@link SparseArray}, the key is the Channel Page
* and the value is the Channel Mask.
*/
@NonNull
@Size(min = 1)
public SparseArray<byte[]> getChannelMask() {
return deepCloneSparseArray(mChannelMask);
}
private static SparseArray<byte[]> deepCloneSparseArray(SparseArray<byte[]> src) {
SparseArray<byte[]> dst = new SparseArray<>(src.size());
for (int i = 0; i < src.size(); i++) {
dst.put(src.keyAt(i), src.valueAt(i).clone());
}
return dst;
}
/** Returns the PSKc. */
@NonNull
@Size(LENGTH_PSKC)
public byte[] getPskc() {
return mPskc.clone();
}
/** Returns the Network Key. */
@NonNull
@Size(LENGTH_NETWORK_KEY)
public byte[] getNetworkKey() {
return mNetworkKey.clone();
}
/**
* Returns the Mesh-local Prefix. The length of the returned prefix is always {@link
* #LENGTH_MESH_LOCAL_PREFIX_BITS}.
*/
@NonNull
public IpPrefix getMeshLocalPrefix() {
return mMeshLocalPrefix;
}
/** Returns the Security Policy. */
@NonNull
public SecurityPolicy getSecurityPolicy() {
return mSecurityPolicy;
}
/**
* Returns Thread TLVs which are not recognized by this device. The returned {@link SparseArray}
* associates TLV values to their keys.
*
* @hide
*/
@NonNull
public SparseArray<byte[]> getUnknownTlvs() {
return deepCloneSparseArray(mUnknownTlvs);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeByteArray(toThreadTlvs());
}
@Override
public boolean equals(Object other) {
if (other == this) {
return true;
} else if (!(other instanceof ActiveOperationalDataset)) {
return false;
} else {
ActiveOperationalDataset otherDataset = (ActiveOperationalDataset) other;
return mActiveTimestamp.equals(otherDataset.mActiveTimestamp)
&& mNetworkName.equals(otherDataset.mNetworkName)
&& Arrays.equals(mExtendedPanId, otherDataset.mExtendedPanId)
&& mPanId == otherDataset.mPanId
&& mChannelPage == otherDataset.mChannelPage
&& mChannel == otherDataset.mChannel
&& areByteSparseArraysEqual(mChannelMask, otherDataset.mChannelMask)
&& Arrays.equals(mPskc, otherDataset.mPskc)
&& Arrays.equals(mNetworkKey, otherDataset.mNetworkKey)
&& mMeshLocalPrefix.equals(otherDataset.mMeshLocalPrefix)
&& mSecurityPolicy.equals(otherDataset.mSecurityPolicy)
&& areByteSparseArraysEqual(mUnknownTlvs, otherDataset.mUnknownTlvs);
}
}
@Override
public int hashCode() {
return deepHashCode(
mActiveTimestamp,
mNetworkName,
mExtendedPanId,
mPanId,
mChannel,
mChannelPage,
mChannelMask,
mPskc,
mNetworkKey,
mMeshLocalPrefix,
mSecurityPolicy);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("{networkName=")
.append(getNetworkName())
.append(", extendedPanId=")
.append(dumpHexString(getExtendedPanId()))
.append(", panId=")
.append(getPanId())
.append(", channel=")
.append(getChannel())
.append(", activeTimestamp=")
.append(getActiveTimestamp())
.append("}");
return sb.toString();
}
/** The builder for creating {@link ActiveOperationalDataset} objects. */
public static final class Builder {
private OperationalDatasetTimestamp mActiveTimestamp;
private String mNetworkName;
private byte[] mExtendedPanId;
private Integer mPanId;
private Integer mChannel;
private Integer mChannelPage;
private SparseArray<byte[]> mChannelMask;
private byte[] mPskc;
private byte[] mNetworkKey;
private IpPrefix mMeshLocalPrefix;
private SecurityPolicy mSecurityPolicy;
private SparseArray<byte[]> mUnknownTlvs;
/**
* Creates a {@link Builder} object with values from an {@link ActiveOperationalDataset}
* object.
*/
public Builder(@NonNull ActiveOperationalDataset activeOpDataset) {
requireNonNull(activeOpDataset, "activeOpDataset cannot be null");
this.mActiveTimestamp = activeOpDataset.mActiveTimestamp;
this.mNetworkName = activeOpDataset.mNetworkName;
this.mExtendedPanId = activeOpDataset.mExtendedPanId.clone();
this.mPanId = activeOpDataset.mPanId;
this.mChannel = activeOpDataset.mChannel;
this.mChannelPage = activeOpDataset.mChannelPage;
this.mChannelMask = deepCloneSparseArray(activeOpDataset.mChannelMask);
this.mPskc = activeOpDataset.mPskc.clone();
this.mNetworkKey = activeOpDataset.mNetworkKey.clone();
this.mMeshLocalPrefix = activeOpDataset.mMeshLocalPrefix;
this.mSecurityPolicy = activeOpDataset.mSecurityPolicy;
this.mUnknownTlvs = deepCloneSparseArray(activeOpDataset.mUnknownTlvs);
}
/**
* Creates an empty {@link Builder} object.
*
* <p>An empty builder cannot build a new {@link ActiveOperationalDataset} object. The
* Active Operational Dataset parameters must be set with setters of this builder.
*/
public Builder() {
mChannelMask = new SparseArray<>();
mUnknownTlvs = new SparseArray<>();
}
/**
* Sets the Active Timestamp.
*
* @param activeTimestamp Active Timestamp of the Operational Dataset
*/
@NonNull
public Builder setActiveTimestamp(@NonNull OperationalDatasetTimestamp activeTimestamp) {
requireNonNull(activeTimestamp, "activeTimestamp cannot be null");
this.mActiveTimestamp = activeTimestamp;
return this;
}
/**
* Sets the Network Name.
*
* @param networkName the name of the Thread network
* @throws IllegalArgumentException if length of the UTF-8 representation of {@code
* networkName} isn't in range of [{@link #LENGTH_MIN_NETWORK_NAME_BYTES}, {@link
* #LENGTH_MAX_NETWORK_NAME_BYTES}].
*/
@NonNull
public Builder setNetworkName(
@NonNull
@Size(
min = LENGTH_MIN_NETWORK_NAME_BYTES,
max = LENGTH_MAX_NETWORK_NAME_BYTES)
String networkName) {
requireNonNull(networkName, "networkName cannot be null");
int nameLength = networkName.getBytes(UTF_8).length;
checkArgument(
nameLength >= LENGTH_MIN_NETWORK_NAME_BYTES
&& nameLength <= LENGTH_MAX_NETWORK_NAME_BYTES,
"Invalid network name (length = %d, expectedLengthRange = [%d, %d])",
nameLength,
LENGTH_MIN_NETWORK_NAME_BYTES,
LENGTH_MAX_NETWORK_NAME_BYTES);
this.mNetworkName = networkName;
return this;
}
/**
* Sets the Extended PAN ID.
*
* <p>Use with caution. A randomly generated Extended PAN ID should be used for real Thread
* networks. It's discouraged to call this method to override the default value created by
* {@link ActiveOperationalDataset#createRandomDataset} in production.
*
* @throws IllegalArgumentException if length of {@code extendedPanId} is not {@link
* #LENGTH_EXTENDED_PAN_ID}.
*/
@NonNull
public Builder setExtendedPanId(
@NonNull @Size(LENGTH_EXTENDED_PAN_ID) byte[] extendedPanId) {
requireNonNull(extendedPanId, "extendedPanId cannot be null");
checkArgument(
extendedPanId.length == LENGTH_EXTENDED_PAN_ID,
"Invalid extended PAN ID (length = %d, expectedLength = %d)",
extendedPanId.length,
LENGTH_EXTENDED_PAN_ID);
this.mExtendedPanId = extendedPanId.clone();
return this;
}
/**
* Sets the PAN ID.
*
* @throws IllegalArgumentException if {@code panId} is not in range of 0x0-0xfffe
*/
@NonNull
public Builder setPanId(@IntRange(from = 0, to = 0xfffe) int panId) {
checkArgument(
panId >= 0 && panId <= 0xfffe,
"PAN ID exceeds allowed range (panid = %d, allowedRange = [0x0, 0xffff])",
panId);
this.mPanId = panId;
return this;
}
/**
* Sets the Channel Page and Channel.
*
* <p>Channel Pages other than {@link #CHANNEL_PAGE_24_GHZ} are undefined and may lead to
* unexpected behavior if it's applied to Thread devices.
*
* @throws IllegalArgumentException if invalid channel is specified for the {@code
* channelPage}
*/
@NonNull
public Builder setChannel(
@IntRange(from = 0, to = 255) int page,
@IntRange(from = 0, to = 65535) int channel) {
checkArgument(
page >= 0 && page <= 255,
"Invalid channel page (page = %d, allowedRange = [0, 255])",
page);
if (page == CHANNEL_PAGE_24_GHZ) {
checkArgument(
channel >= CHANNEL_MIN_24_GHZ && channel <= CHANNEL_MAX_24_GHZ,
"Invalid channel %d in page %d (allowedChannelRange = [%d, %d])",
channel,
page,
CHANNEL_MIN_24_GHZ,
CHANNEL_MAX_24_GHZ);
} else {
checkArgument(
channel >= 0 && channel <= 65535,
"Invalid channel %d in page %d "
+ "(channel = %d, allowedChannelRange = [0, 65535])",
channel,
page,
channel);
}
this.mChannelPage = page;
this.mChannel = channel;
return this;
}
/**
* Sets the Channel Mask.
*
* @throws IllegalArgumentException if {@code channelMask} is empty
*/
@NonNull
public Builder setChannelMask(@NonNull @Size(min = 1) SparseArray<byte[]> channelMask) {
requireNonNull(channelMask, "channelMask cannot be null");
checkArgument(channelMask.size() > 0, "channelMask is empty");
this.mChannelMask = deepCloneSparseArray(channelMask);
return this;
}
/**
* Sets the PSKc.
*
* <p>Use with caution. A randomly generated PSKc should be used for real Thread networks.
* It's discouraged to call this method to override the default value created by {@link
* ActiveOperationalDataset#createRandomDataset} in production.
*
* @param pskc the key stretched version of the Commissioning Credential for the network
* @throws IllegalArgumentException if length of {@code pskc} is not {@link #LENGTH_PSKC}
*/
@NonNull
public Builder setPskc(@NonNull @Size(LENGTH_PSKC) byte[] pskc) {
requireNonNull(pskc, "pskc cannot be null");
checkArgument(
pskc.length == LENGTH_PSKC,
"Invalid PSKc length (length = %d, expectedLength = %d)",
pskc.length,
LENGTH_PSKC);
this.mPskc = pskc.clone();
return this;
}
/**
* Sets the Network Key.
*
* <p>Use with caution, randomly generated Network Key should be used for real Thread
* networks. It's discouraged to call this method to override the default value created by
* {@link ActiveOperationalDataset#createRandomDataset} in production.
*
* @param networkKey a 128-bit security key-derivation key for the Thread Network
* @throws IllegalArgumentException if length of {@code networkKey} is not {@link
* #LENGTH_NETWORK_KEY}
*/
@NonNull
public Builder setNetworkKey(@NonNull @Size(LENGTH_NETWORK_KEY) byte[] networkKey) {
requireNonNull(networkKey, "networkKey cannot be null");
checkArgument(
networkKey.length == LENGTH_NETWORK_KEY,
"Invalid network key length (length = %d, expectedLength = %d)",
networkKey.length,
LENGTH_NETWORK_KEY);
this.mNetworkKey = networkKey.clone();
return this;
}
/**
* Sets the Mesh-Local Prefix.
*
* @param meshLocalPrefix the prefix used for realm-local traffic within the mesh
* @throws IllegalArgumentException if prefix length of {@code meshLocalPrefix} isn't {@link
* #LENGTH_MESH_LOCAL_PREFIX_BITS} or {@code meshLocalPrefix} doesn't start with {@code
* 0xfd}
*/
@NonNull
public Builder setMeshLocalPrefix(@NonNull IpPrefix meshLocalPrefix) {
requireNonNull(meshLocalPrefix, "meshLocalPrefix cannot be null");
checkArgument(
meshLocalPrefix.getPrefixLength() == LENGTH_MESH_LOCAL_PREFIX_BITS,
"Invalid mesh-local prefix length (length = %d, expectedLength = %d)",
meshLocalPrefix.getPrefixLength(),
LENGTH_MESH_LOCAL_PREFIX_BITS);
checkArgument(
meshLocalPrefix.getRawAddress()[0] == MESH_LOCAL_PREFIX_FIRST_BYTE,
"Mesh-local prefix must start with 0xfd: " + meshLocalPrefix);
this.mMeshLocalPrefix = meshLocalPrefix;
return this;
}
@NonNull
private Builder setMeshLocalPrefix(byte[] meshLocalPrefix) {
final int prefixLength = meshLocalPrefix.length * 8;
checkArgument(
prefixLength == LENGTH_MESH_LOCAL_PREFIX_BITS,
"Invalid mesh-local prefix length (length = %d, expectedLength = %d)",
prefixLength,
LENGTH_MESH_LOCAL_PREFIX_BITS);
byte[] ip6RawAddress = new byte[16];
System.arraycopy(meshLocalPrefix, 0, ip6RawAddress, 0, meshLocalPrefix.length);
try {
return setMeshLocalPrefix(
new IpPrefix(Inet6Address.getByAddress(ip6RawAddress), prefixLength));
} catch (UnknownHostException e) {
// Can't happen because numeric address is provided
throw new AssertionError(e);
}
}
/** Sets the Security Policy. */
@NonNull
public Builder setSecurityPolicy(@NonNull SecurityPolicy securityPolicy) {
requireNonNull(securityPolicy, "securityPolicy cannot be null");
this.mSecurityPolicy = securityPolicy;
return this;
}
/**
* Sets additional unknown TLVs.
*
* @hide
*/
@NonNull
public Builder setUnknownTlvs(@NonNull SparseArray<byte[]> unknownTlvs) {
requireNonNull(unknownTlvs, "unknownTlvs cannot be null");
mUnknownTlvs = deepCloneSparseArray(unknownTlvs);
return this;
}
/** Adds one more unknown TLV. @hide */
@VisibleForTesting
@NonNull
public Builder addUnknownTlv(int type, byte[] value) {
mUnknownTlvs.put(type, value);
return this;
}
/**
* Creates a new {@link ActiveOperationalDataset} object.
*
* @throws IllegalStateException if any of the fields isn't set or the total length exceeds
* {@link #LENGTH_MAX_DATASET_TLVS} bytes
*/
@NonNull
public ActiveOperationalDataset build() {
checkState(mActiveTimestamp != null, "Active Timestamp is missing");
checkState(mNetworkName != null, "Network Name is missing");
checkState(mExtendedPanId != null, "Extended PAN ID is missing");
checkState(mPanId != null, "PAN ID is missing");
checkState(mChannel != null, "Channel is missing");
checkState(mChannelPage != null, "Channel Page is missing");
checkState(mChannelMask.size() != 0, "Channel Mask is missing");
checkState(mPskc != null, "PSKc is missing");
checkState(mNetworkKey != null, "Network Key is missing");
checkState(mMeshLocalPrefix != null, "Mesh Local Prefix is missing");
checkState(mSecurityPolicy != null, "Security Policy is missing");
int length = getTotalDatasetLength();
if (length > LENGTH_MAX_DATASET_TLVS) {
throw new IllegalStateException(
String.format(
"Total dataset length exceeds max length %d (actual is %d)",
LENGTH_MAX_DATASET_TLVS, length));
}
return new ActiveOperationalDataset(this);
}
private int getTotalDatasetLength() {
int length =
2 * 9 // 9 fields with 1 byte of type and 1 byte of length
+ OperationalDatasetTimestamp.LENGTH_TIMESTAMP
+ mNetworkName.getBytes(UTF_8).length
+ LENGTH_EXTENDED_PAN_ID
+ LENGTH_PAN_ID
+ LENGTH_CHANNEL
+ LENGTH_PSKC
+ LENGTH_NETWORK_KEY
+ LENGTH_MESH_LOCAL_PREFIX_BITS / 8
+ mSecurityPolicy.toTlvValue().length;
for (int i = 0; i < mChannelMask.size(); i++) {
length += 2 + mChannelMask.valueAt(i).length;
}
// For the type and length bytes of the Channel Mask TLV because the masks are encoded
// as TLVs in TLV.
length += 2;
for (int i = 0; i < mUnknownTlvs.size(); i++) {
length += 2 + mUnknownTlvs.valueAt(i).length;
}
return length;
}
}
/**
* The Security Policy of Thread Operational Dataset which provides an administrator with a way
* to enable or disable certain security related behaviors.
*/
public static final class SecurityPolicy {
/** The default Rotation Time in hours. */
public static final int DEFAULT_ROTATION_TIME_HOURS = 672;
/** The minimum length of Security Policy flags in bytes. */
public static final int LENGTH_MIN_SECURITY_POLICY_FLAGS = 1;
/** The length of Rotation Time TLV value in bytes. */
private static final int LENGTH_SECURITY_POLICY_ROTATION_TIME = 2;
private final int mRotationTimeHours;
private final byte[] mFlags;
/**
* Creates a new {@link SecurityPolicy} object.
*
* @param rotationTimeHours the value for Thread key rotation in hours. Must be in range of
* 0x1-0xffff.
* @param flags security policy flags with length of either 1 byte for Thread 1.1 or 2 bytes
* for Thread 1.2 or higher.
* @throws IllegalArgumentException if {@code rotationTimeHours} is not in range of
* 0x1-0xffff or length of {@code flags} is smaller than {@link
* #LENGTH_MIN_SECURITY_POLICY_FLAGS}.
*/
public SecurityPolicy(
@IntRange(from = 0x1, to = 0xffff) int rotationTimeHours,
@NonNull @Size(min = LENGTH_MIN_SECURITY_POLICY_FLAGS) byte[] flags) {
requireNonNull(flags, "flags cannot be null");
checkArgument(
rotationTimeHours >= 1 && rotationTimeHours <= 0xffff,
"Rotation time exceeds allowed range (rotationTimeHours = %d, allowedRange ="
+ " [0x1, 0xffff])",
rotationTimeHours);
checkArgument(
flags.length >= LENGTH_MIN_SECURITY_POLICY_FLAGS,
"Invalid security policy flags length (length = %d, minimumLength = %d)",
flags.length,
LENGTH_MIN_SECURITY_POLICY_FLAGS);
this.mRotationTimeHours = rotationTimeHours;
this.mFlags = flags.clone();
}
/**
* Creates a new {@link SecurityPolicy} object from the Security Policy TLV value.
*
* @hide
*/
@VisibleForTesting
@NonNull
public static SecurityPolicy fromTlvValue(byte[] encodedSecurityPolicy) {
checkArgument(
encodedSecurityPolicy.length
>= LENGTH_SECURITY_POLICY_ROTATION_TIME
+ LENGTH_MIN_SECURITY_POLICY_FLAGS,
"Invalid Security Policy TLV length (length = %d, minimumLength = %d)",
encodedSecurityPolicy.length,
LENGTH_SECURITY_POLICY_ROTATION_TIME + LENGTH_MIN_SECURITY_POLICY_FLAGS);
return new SecurityPolicy(
((encodedSecurityPolicy[0] & 0xff) << 8) | (encodedSecurityPolicy[1] & 0xff),
Arrays.copyOfRange(
encodedSecurityPolicy,
LENGTH_SECURITY_POLICY_ROTATION_TIME,
encodedSecurityPolicy.length));
}
/**
* Converts this {@link SecurityPolicy} object to Security Policy TLV value.
*
* @hide
*/
@VisibleForTesting
@NonNull
public byte[] toTlvValue() {
ByteArrayOutputStream result = new ByteArrayOutputStream();
result.write(mRotationTimeHours >> 8);
result.write(mRotationTimeHours);
result.write(mFlags, 0, mFlags.length);
return result.toByteArray();
}
/** Returns the Security Policy Rotation Time in hours. */
@IntRange(from = 0x1, to = 0xffff)
public int getRotationTimeHours() {
return mRotationTimeHours;
}
/** Returns 1 byte flags for Thread 1.1 or 2 bytes flags for Thread 1.2. */
@NonNull
@Size(min = LENGTH_MIN_SECURITY_POLICY_FLAGS)
public byte[] getFlags() {
return mFlags.clone();
}
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
} else if (!(other instanceof SecurityPolicy)) {
return false;
} else {
SecurityPolicy otherSecurityPolicy = (SecurityPolicy) other;
return mRotationTimeHours == otherSecurityPolicy.mRotationTimeHours
&& Arrays.equals(mFlags, otherSecurityPolicy.mFlags);
}
}
@Override
public int hashCode() {
return deepHashCode(mRotationTimeHours, mFlags);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("{rotation=")
.append(mRotationTimeHours)
.append(", flags=")
.append(dumpHexString(mFlags))
.append("}");
return sb.toString();
}
}
}
/*
* 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.android.internal.util.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;
import android.annotation.FlaggedApi;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.Objects;
/**
* The timestamp of Thread Operational Dataset.
*
* @see ActiveOperationalDataset
* @see PendingOperationalDataset
* @hide
*/
@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
@SystemApi
public final class OperationalDatasetTimestamp {
/** @hide */
public static final int LENGTH_TIMESTAMP = Long.BYTES;
private static final long TICKS_UPPER_BOUND = 0x8000;
private final Instant mInstant;
private final boolean mIsAuthoritativeSource;
/**
* Creates a new {@link OperationalDatasetTimestamp} object from an {@link Instant}.
*
* <p>The {@code seconds} is set to {@code instant.getEpochSecond()}, {@code ticks} is set to
* {@link instant#getNano()} based on frequency of 32768 Hz, and {@code isAuthoritativeSource}
* is set to {@code true}.
*
* @throws IllegalArgumentException if {@code instant.getEpochSecond()} is larger than {@code
* 0xffffffffffffL}
*/
@NonNull
public static OperationalDatasetTimestamp fromInstant(@NonNull Instant instant) {
return new OperationalDatasetTimestamp(instant, /* isAuthoritativeSource= */ true);
}
/** Converts this {@link OperationalDatasetTimestamp} object to an {@link Instant}. */
@NonNull
public Instant toInstant() {
return mInstant;
}
/**
* Creates a new {@link OperationalDatasetTimestamp} object from the OperationalDatasetTimestamp
* TLV value.
*
* @hide
*/
@NonNull
public static OperationalDatasetTimestamp fromTlvValue(@NonNull byte[] encodedTimestamp) {
requireNonNull(encodedTimestamp, "encodedTimestamp cannot be null");
checkArgument(
encodedTimestamp.length == LENGTH_TIMESTAMP,
"Invalid Thread OperationalDatasetTimestamp length (length = %d,"
+ " expectedLength=%d)",
encodedTimestamp.length,
LENGTH_TIMESTAMP);
long longTimestamp = ByteBuffer.wrap(encodedTimestamp).getLong();
return new OperationalDatasetTimestamp(
(longTimestamp >> 16) & 0x0000ffffffffffffL,
(int) ((longTimestamp >> 1) & 0x7fffL),
(longTimestamp & 0x01) != 0);
}
/**
* Converts this {@link OperationalDatasetTimestamp} object to Thread TLV value.
*
* @hide
*/
@NonNull
public byte[] toTlvValue() {
byte[] tlv = new byte[LENGTH_TIMESTAMP];
ByteBuffer buffer = ByteBuffer.wrap(tlv);
long encodedValue =
(mInstant.getEpochSecond() << 16)
| ((mInstant.getNano() * TICKS_UPPER_BOUND / 1000000000L) << 1)
| (mIsAuthoritativeSource ? 1 : 0);
buffer.putLong(encodedValue);
return tlv;
}
/**
* Creates a new {@link OperationalDatasetTimestamp} object.
*
* @param seconds the value encodes a Unix Time value. Must be in the range of
* 0x0-0xffffffffffffL
* @param ticks the value encodes the fractional Unix Time value in 32.768 kHz resolution. Must
* be in the range of 0x0-0x7fff
* @param isAuthoritativeSource the flag indicates the time was obtained from an authoritative
* source: either NTP (Network Time Protocol), GPS (Global Positioning System), cell
* network, or other method
* @throws IllegalArgumentException if the {@code seconds} is not in range of
* 0x0-0xffffffffffffL or {@code ticks} is not in range of 0x0-0x7fff
*/
public OperationalDatasetTimestamp(
@IntRange(from = 0x0, to = 0xffffffffffffL) long seconds,
@IntRange(from = 0x0, to = 0x7fff) int ticks,
boolean isAuthoritativeSource) {
this(makeInstant(seconds, ticks), isAuthoritativeSource);
}
private static Instant makeInstant(long seconds, int ticks) {
checkArgument(
seconds >= 0 && seconds <= 0xffffffffffffL,
"seconds exceeds allowed range (seconds = %d,"
+ " allowedRange = [0x0, 0xffffffffffffL])",
seconds);
checkArgument(
ticks >= 0 && ticks <= 0x7fff,
"ticks exceeds allowed ranged (ticks = %d, allowedRange" + " = [0x0, 0x7fff])",
ticks);
long nanos = Math.round((double) ticks * 1000000000L / TICKS_UPPER_BOUND);
return Instant.ofEpochSecond(seconds, nanos);
}
/**
* Creates new {@link OperationalDatasetTimestamp} object.
*
* @throws IllegalArgumentException if {@code instant.getEpochSecond()} is larger than {@code
* 0xffffffffffffL}
*/
private OperationalDatasetTimestamp(@NonNull Instant instant, boolean isAuthoritativeSource) {
requireNonNull(instant, "instant cannot be null");
long seconds = instant.getEpochSecond();
checkArgument(
seconds >= 0 && seconds <= 0xffffffffffffL,
"instant seconds exceeds allowed range (seconds = %d, allowedRange = [0x0,"
+ " 0xffffffffffffL])",
seconds);
mInstant = instant;
mIsAuthoritativeSource = isAuthoritativeSource;
}
/**
* Returns the rounded ticks converted from the nano seconds.
*
* <p>Note that rhe return value can be as large as {@code TICKS_UPPER_BOUND}.
*/
private static int getRoundedTicks(long nanos) {
return (int) Math.round((double) nanos * TICKS_UPPER_BOUND / 1000000000L);
}
/** Returns the seconds portion of the timestamp. */
public @IntRange(from = 0x0, to = 0xffffffffffffL) long getSeconds() {
return mInstant.getEpochSecond() + getRoundedTicks(mInstant.getNano()) / TICKS_UPPER_BOUND;
}
/** Returns the ticks portion of the timestamp. */
public @IntRange(from = 0x0, to = 0x7fff) int getTicks() {
// the rounded ticks can be 0x8000 if mInstant.getNano() >= 999984742
return (int) (getRoundedTicks(mInstant.getNano()) % TICKS_UPPER_BOUND);
}
/** Returns {@code true} if the timestamp comes from an authoritative source. */
public boolean isAuthoritativeSource() {
return mIsAuthoritativeSource;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("{seconds=")
.append(getSeconds())
.append(", ticks=")
.append(getTicks())
.append(", isAuthoritativeSource=")
.append(isAuthoritativeSource())
.append(", instant=")
.append(toInstant())
.append("}");
return sb.toString();
}
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
} else if (!(other instanceof OperationalDatasetTimestamp)) {
return false;
} else {
OperationalDatasetTimestamp otherTimestamp = (OperationalDatasetTimestamp) other;
return mInstant.equals(otherTimestamp.mInstant)
&& mIsAuthoritativeSource == otherTimestamp.mIsAuthoritativeSource;
}
}
@Override
public int hashCode() {
return Objects.hash(mInstant, mIsAuthoritativeSource);
}
}
/*
* Copyright 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;
parcelable PendingOperationalDataset;
/*
* 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.android.internal.util.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.SparseArray;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.Objects;
/**
* Data interface for managing a Thread Pending Operational Dataset.
*
* <p>The Pending Operational Dataset represents an Operational Dataset which will become Active in
* a given delay. This is typically used to deploy new network parameters (e.g. Network Key or
* Channel) to all devices in the network.
*
* @hide
*/
@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
@SystemApi
public final class PendingOperationalDataset implements Parcelable {
// Value defined in Thread spec 8.10.1.16
private static final int TYPE_PENDING_TIMESTAMP = 51;
// Values defined in Thread spec 8.10.1.17
private static final int TYPE_DELAY_TIMER = 52;
private static final int LENGTH_DELAY_TIMER_BYTES = 4;
@NonNull
public static final Creator<PendingOperationalDataset> CREATOR =
new Creator<>() {
@Override
public PendingOperationalDataset createFromParcel(Parcel in) {
return PendingOperationalDataset.fromThreadTlvs(in.createByteArray());
}
@Override
public PendingOperationalDataset[] newArray(int size) {
return new PendingOperationalDataset[size];
}
};
@NonNull private final ActiveOperationalDataset mActiveOpDataset;
@NonNull private final OperationalDatasetTimestamp mPendingTimestamp;
@NonNull private final Duration mDelayTimer;
/** Creates a new {@link PendingOperationalDataset} object. */
public PendingOperationalDataset(
@NonNull ActiveOperationalDataset activeOpDataset,
@NonNull OperationalDatasetTimestamp pendingTimestamp,
@NonNull Duration delayTimer) {
requireNonNull(activeOpDataset, "activeOpDataset cannot be null");
requireNonNull(pendingTimestamp, "pendingTimestamp cannot be null");
requireNonNull(delayTimer, "delayTimer cannot be null");
this.mActiveOpDataset = activeOpDataset;
this.mPendingTimestamp = pendingTimestamp;
this.mDelayTimer = delayTimer;
}
/**
* Creates a new {@link PendingOperationalDataset} object from a series of Thread TLVs.
*
* <p>{@code tlvs} can be obtained from the value of a Thread Pending Operational Dataset TLV
* (see the <a href="https://www.threadgroup.org/support#specifications">Thread
* specification</a> for the definition) or the return value of {@link #toThreadTlvs}.
*
* @throws IllegalArgumentException if {@code tlvs} is malformed or contains an invalid Thread
* TLV
*/
@NonNull
public static PendingOperationalDataset fromThreadTlvs(@NonNull byte[] tlvs) {
requireNonNull(tlvs, "tlvs cannot be null");
SparseArray<byte[]> newUnknownTlvs = new SparseArray<>();
OperationalDatasetTimestamp pendingTimestamp = null;
Duration delayTimer = null;
ActiveOperationalDataset activeDataset = ActiveOperationalDataset.fromThreadTlvs(tlvs);
SparseArray<byte[]> unknownTlvs = activeDataset.getUnknownTlvs();
for (int i = 0; i < unknownTlvs.size(); i++) {
int key = unknownTlvs.keyAt(i);
byte[] value = unknownTlvs.valueAt(i);
switch (key) {
case TYPE_PENDING_TIMESTAMP:
pendingTimestamp = OperationalDatasetTimestamp.fromTlvValue(value);
break;
case TYPE_DELAY_TIMER:
checkArgument(
value.length == LENGTH_DELAY_TIMER_BYTES,
"Invalid delay timer (length = %d, expectedLength = %d)",
value.length,
LENGTH_DELAY_TIMER_BYTES);
int millis = ByteBuffer.wrap(value).getInt();
delayTimer = Duration.ofMillis(Integer.toUnsignedLong(millis));
break;
default:
newUnknownTlvs.put(key, value);
break;
}
}
if (pendingTimestamp == null) {
throw new IllegalArgumentException("Pending Timestamp is missing");
}
if (delayTimer == null) {
throw new IllegalArgumentException("Delay Timer is missing");
}
activeDataset =
new ActiveOperationalDataset.Builder(activeDataset)
.setUnknownTlvs(newUnknownTlvs)
.build();
return new PendingOperationalDataset(activeDataset, pendingTimestamp, delayTimer);
}
/** Returns the Active Operational Dataset. */
@NonNull
public ActiveOperationalDataset getActiveOperationalDataset() {
return mActiveOpDataset;
}
/** Returns the Pending Timestamp. */
@NonNull
public OperationalDatasetTimestamp getPendingTimestamp() {
return mPendingTimestamp;
}
/** Returns the Delay Timer. */
@NonNull
public Duration getDelayTimer() {
return mDelayTimer;
}
/**
* Converts this {@link PendingOperationalDataset} object to a series of Thread TLVs.
*
* <p>See the <a href="https://www.threadgroup.org/support#specifications">Thread
* specification</a> for the definition of the Thread TLV format.
*/
@NonNull
public byte[] toThreadTlvs() {
ByteArrayOutputStream dataset = new ByteArrayOutputStream();
byte[] activeDatasetBytes = mActiveOpDataset.toThreadTlvs();
dataset.write(activeDatasetBytes, 0, activeDatasetBytes.length);
dataset.write(TYPE_PENDING_TIMESTAMP);
byte[] pendingTimestampBytes = mPendingTimestamp.toTlvValue();
dataset.write(pendingTimestampBytes.length);
dataset.write(pendingTimestampBytes, 0, pendingTimestampBytes.length);
dataset.write(TYPE_DELAY_TIMER);
byte[] delayTimerBytes = new byte[LENGTH_DELAY_TIMER_BYTES];
ByteBuffer.wrap(delayTimerBytes).putInt((int) mDelayTimer.toMillis());
dataset.write(delayTimerBytes.length);
dataset.write(delayTimerBytes, 0, delayTimerBytes.length);
return dataset.toByteArray();
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
} else if (!(other instanceof PendingOperationalDataset)) {
return false;
} else {
PendingOperationalDataset otherDataset = (PendingOperationalDataset) other;
return mActiveOpDataset.equals(otherDataset.mActiveOpDataset)
&& mPendingTimestamp.equals(otherDataset.mPendingTimestamp)
&& mDelayTimer.equals(otherDataset.mDelayTimer);
}
}
@Override
public int hashCode() {
return Objects.hash(mActiveOpDataset, mPendingTimestamp, mDelayTimer);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("{activeDataset=")
.append(getActiveOperationalDataset())
.append(", pendingTimestamp=")
.append(getPendingTimestamp())
.append(", delayTimer=")
.append(getDelayTimer())
.append("}");
return sb.toString();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeByteArray(toThreadTlvs());
}
}
...@@ -37,6 +37,7 @@ android_test { ...@@ -37,6 +37,7 @@ android_test {
"androidx.test.ext.junit", "androidx.test.ext.junit",
"compatibility-device-util-axt", "compatibility-device-util-axt",
"ctstestrunner-axt", "ctstestrunner-axt",
"guava-android-testlib",
"net-tests-utils", "net-tests-utils",
"truth", "truth",
], ],
......
...@@ -47,5 +47,7 @@ ...@@ -47,5 +47,7 @@
<test class="com.android.tradefed.testtype.AndroidJUnitTest" > <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="android.net.thread.cts" /> <option name="package" value="android.net.thread.cts" />
<!-- Ignores tests introduced by guava-android-testlib -->
<option name="exclude-annotation" value="org.junit.Ignore"/>
</test> </test>
</configuration> </configuration>
/*
* 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.cts;
import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.net.IpPrefix;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.ActiveOperationalDataset.Builder;
import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
import android.net.thread.OperationalDatasetTimestamp;
import android.util.SparseArray;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.google.common.primitives.Bytes;
import com.google.common.testing.EqualsTester;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
/** CTS tests for {@link ActiveOperationalDataset}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
public final class ActiveOperationalDatasetTest {
private static final int TYPE_ACTIVE_TIMESTAMP = 14;
private static final int TYPE_CHANNEL = 0;
private static final int TYPE_CHANNEL_MASK = 53;
private static final int TYPE_EXTENDED_PAN_ID = 2;
private static final int TYPE_MESH_LOCAL_PREFIX = 7;
private static final int TYPE_NETWORK_KEY = 5;
private static final int TYPE_NETWORK_NAME = 3;
private static final int TYPE_PAN_ID = 1;
private static final int TYPE_PSKC = 4;
private static final int TYPE_SECURITY_POLICY = 12;
// 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");
private static byte[] removeTlv(byte[] dataset, int type) {
ByteArrayOutputStream os = new ByteArrayOutputStream(dataset.length);
int i = 0;
while (i < dataset.length) {
int ty = dataset[i++] & 0xff;
byte length = dataset[i++];
if (ty != type) {
byte[] value = Arrays.copyOfRange(dataset, i, i + length);
os.write(ty);
os.write(length);
os.writeBytes(value);
}
i += length;
}
return os.toByteArray();
}
private static byte[] addTlv(byte[] dataset, String tlvHex) {
return Bytes.concat(dataset, base16().decode(tlvHex));
}
private static byte[] replaceTlv(byte[] dataset, int type, String newTlvHex) {
return addTlv(removeTlv(dataset, type), newTlvHex);
}
@Test
public void parcelable_parcelingIsLossLess() {
ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET);
assertParcelingIsLossless(dataset);
}
@Test
public void fromThreadTlvs_tooLongTlv_throwsIllegalArgument() {
byte[] invalidTlv = new byte[255];
invalidTlv[0] = (byte) 0xff;
// This is invalid because the TLV has max total length of 254 bytes and the value length
// can't exceeds 252 ( = 254 - 1 - 1)
invalidTlv[1] = (byte) 253;
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_invalidNetworkKeyTlv_throwsIllegalArgument() {
byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_NETWORK_KEY, "05080000000000000000");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_noNetworkKeyTlv_throwsIllegalArgument() {
byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_NETWORK_KEY);
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_invalidActiveTimestampTlv_throwsIllegalArgument() {
byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_ACTIVE_TIMESTAMP, "0E0700000000010000");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_noActiveTimestampTlv_throwsIllegalArgument() {
byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_ACTIVE_TIMESTAMP);
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_invalidNetworkNameTlv_emptyName_throwsIllegalArgument() {
byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_NETWORK_NAME, "0300");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_invalidNetworkNameTlv_tooLongName_throwsIllegalArgument() {
byte[] invalidTlv =
replaceTlv(
VALID_DATASET, TYPE_NETWORK_NAME, "03114142434445464748494A4B4C4D4E4F5051");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_noNetworkNameTlv_throwsIllegalArgument() {
byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_NETWORK_NAME);
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_invalidChannelTlv_channelMissing_throwsIllegalArgument() {
byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "000100");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_undefinedChannelPage_success() {
byte[] datasetTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "0003010020");
ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(datasetTlv);
assertThat(dataset.getChannelPage()).isEqualTo(0x01);
assertThat(dataset.getChannel()).isEqualTo(0x20);
}
@Test
public void fromThreadTlvs_invalid2P4GhzChannel_throwsIllegalArgument() {
byte[] invalidTlv1 = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "000300000A");
byte[] invalidTlv2 = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "000300001B");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv1));
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv2));
}
@Test
public void fromThreadTlvs_valid2P4GhzChannelTlv_success() {
byte[] validTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "0003000010");
ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(validTlv);
assertThat(dataset.getChannel()).isEqualTo(16);
}
@Test
public void fromThreadTlvs_noChannelTlv_throwsIllegalArgument() {
byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_CHANNEL);
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_prematureEndOfChannelMaskEntry_throwsIllegalArgument() {
byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL_MASK, "350100");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_inconsistentChannelMaskLength_throwsIllegalArgument() {
byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL_MASK, "3506000500010000");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_unsupportedChannelMaskLength_success() {
ActiveOperationalDataset dataset =
ActiveOperationalDataset.fromThreadTlvs(
replaceTlv(VALID_DATASET, TYPE_CHANNEL_MASK, "350700050001000000"));
SparseArray<byte[]> channelMask = dataset.getChannelMask();
assertThat(channelMask.size()).isEqualTo(1);
assertThat(channelMask.get(CHANNEL_PAGE_24_GHZ))
.isEqualTo(new byte[] {0x00, 0x01, 0x00, 0x00, 0x00});
}
@Test
public void fromThreadTlvs_noChannelMaskTlv_throwsIllegalArgument() {
byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_CHANNEL_MASK);
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_invalidPanIdTlv_throwsIllegalArgument() {
byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_PAN_ID, "010101");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_noPanIdTlv_throwsIllegalArgument() {
byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_PAN_ID);
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_invalidExtendedPanIdTlv_throwsIllegalArgument() {
byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_EXTENDED_PAN_ID, "020700010203040506");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_noExtendedPanIdTlv_throwsIllegalArgument() {
byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_EXTENDED_PAN_ID);
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_invalidPskcTlv_throwsIllegalArgument() {
byte[] invalidTlv =
replaceTlv(VALID_DATASET, TYPE_PSKC, "0411000102030405060708090A0B0C0D0E0F10");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_noPskcTlv_throwsIllegalArgument() {
byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_PSKC);
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_invalidMeshLocalPrefixTlv_throwsIllegalArgument() {
byte[] invalidTlv =
replaceTlv(VALID_DATASET, TYPE_MESH_LOCAL_PREFIX, "0709FD0001020304050607");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_noMeshLocalPrefixTlv_throwsIllegalArgument() {
byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_MESH_LOCAL_PREFIX);
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_tooShortSecurityPolicyTlv_throwsIllegalArgument() {
byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_SECURITY_POLICY, "0C0101");
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_noSecurityPolicyTlv_throwsIllegalArgument() {
byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_SECURITY_POLICY);
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
}
@Test
public void fromThreadTlvs_lengthAndDataMissing_throwsIllegalArgument() {
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(new byte[] {(byte) 0x00}));
}
@Test
public void fromThreadTlvs_prematureEndOfData_throwsIllegalArgument() {
assertThrows(
IllegalArgumentException.class,
() -> ActiveOperationalDataset.fromThreadTlvs(new byte[] {0x00, 0x03, 0x00, 0x00}));
}
@Test
public void fromThreadTlvs_validFullDataset_success() {
// A valid Thread active operational dataset:
// 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
byte[] validDatasetTlv =
base16().decode(
"0E080000000000010000000300001335060004001FFFE002"
+ "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ "B9D351B40C0402A0FFF8");
ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(validDatasetTlv);
assertThat(dataset.getNetworkKey())
.isEqualTo(base16().decode("F26B3153760F519A63BAFDDFFC80D2AF"));
assertThat(dataset.getPanId()).isEqualTo(0xd9a0);
assertThat(dataset.getExtendedPanId()).isEqualTo(base16().decode("ACC214689BC40BDF"));
assertThat(dataset.getChannel()).isEqualTo(19);
assertThat(dataset.getNetworkName()).isEqualTo("OpenThread-d9a0");
assertThat(dataset.getPskc())
.isEqualTo(base16().decode("A245479C836D551B9CA557F7B9D351B4"));
assertThat(dataset.getActiveTimestamp())
.isEqualTo(new OperationalDatasetTimestamp(1, 0, false));
SparseArray<byte[]> channelMask = dataset.getChannelMask();
assertThat(channelMask.size()).isEqualTo(1);
assertThat(channelMask.get(CHANNEL_PAGE_24_GHZ))
.isEqualTo(new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0});
assertThat(dataset.getMeshLocalPrefix())
.isEqualTo(new IpPrefix("fd64:db12:25f4:7e0b::/64"));
assertThat(dataset.getSecurityPolicy())
.isEqualTo(new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}));
}
@Test
public void fromThreadTlvs_containsUnknownTlvs_unknownTlvsRetained() {
final byte[] datasetWithUnknownTlvs = addTlv(VALID_DATASET, "AA01FFBB020102");
ActiveOperationalDataset dataset =
ActiveOperationalDataset.fromThreadTlvs(datasetWithUnknownTlvs);
byte[] newDatasetTlvs = dataset.toThreadTlvs();
String newDatasetTlvsHex = base16().encode(newDatasetTlvs);
assertThat(newDatasetTlvs.length).isEqualTo(datasetWithUnknownTlvs.length);
assertThat(newDatasetTlvsHex).contains("AA01FF");
assertThat(newDatasetTlvsHex).contains("BB020102");
}
@Test
public void toThreadTlvs_conversionIsLossLess() {
ActiveOperationalDataset dataset1 = ActiveOperationalDataset.createRandomDataset();
ActiveOperationalDataset dataset2 =
ActiveOperationalDataset.fromThreadTlvs(dataset1.toThreadTlvs());
assertThat(dataset2).isEqualTo(dataset1);
}
@Test
public void builder_buildWithdefaultValues_throwsIllegalState() {
assertThrows(IllegalStateException.class, () -> new Builder().build());
}
@Test
public void builder_setValidNetworkKey_success() {
final byte[] networkKey =
new byte[] {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
0x0d, 0x0e, 0x0f
};
ActiveOperationalDataset dataset =
new Builder(ActiveOperationalDataset.createRandomDataset())
.setNetworkKey(networkKey)
.build();
assertThat(dataset.getNetworkKey()).isEqualTo(networkKey);
}
@Test
public void builder_setInvalidNetworkKey_throwsIllegalArgument() {
byte[] invalidNetworkKey = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
assertThrows(
IllegalArgumentException.class, () -> builder.setNetworkKey(invalidNetworkKey));
}
@Test
public void builder_setValidExtendedPanId_success() {
byte[] extendedPanId = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
ActiveOperationalDataset dataset =
new Builder(ActiveOperationalDataset.createRandomDataset())
.setExtendedPanId(extendedPanId)
.build();
assertThat(dataset.getExtendedPanId()).isEqualTo(extendedPanId);
}
@Test
public void builder_setInvalidExtendedPanId_throwsIllegalArgument() {
byte[] extendedPanId = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
assertThrows(IllegalArgumentException.class, () -> builder.setExtendedPanId(extendedPanId));
}
@Test
public void builder_setValidPanId_success() {
ActiveOperationalDataset dataset =
new Builder(ActiveOperationalDataset.createRandomDataset())
.setPanId(0xfffe)
.build();
assertThat(dataset.getPanId()).isEqualTo(0xfffe);
}
@Test
public void builder_setInvalidPanId_throwsIllegalArgument() {
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
assertThrows(IllegalArgumentException.class, () -> builder.setPanId(0xffff));
}
@Test
public void builder_setInvalidChannel_throwsIllegalArgument() {
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
assertThrows(IllegalArgumentException.class, () -> builder.setChannel(0, 0));
assertThrows(IllegalArgumentException.class, () -> builder.setChannel(0, 27));
}
@Test
public void builder_setValid2P4GhzChannel_success() {
ActiveOperationalDataset dataset =
new Builder(ActiveOperationalDataset.createRandomDataset())
.setChannel(CHANNEL_PAGE_24_GHZ, 16)
.build();
assertThat(dataset.getChannel()).isEqualTo(16);
assertThat(dataset.getChannelPage()).isEqualTo(CHANNEL_PAGE_24_GHZ);
}
@Test
public void builder_setValidNetworkName_success() {
ActiveOperationalDataset dataset =
new Builder(ActiveOperationalDataset.createRandomDataset())
.setNetworkName("ot-network")
.build();
assertThat(dataset.getNetworkName()).isEqualTo("ot-network");
}
@Test
public void builder_setEmptyNetworkName_throwsIllegalArgument() {
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
assertThrows(IllegalArgumentException.class, () -> builder.setNetworkName(""));
}
@Test
public void builder_setTooLongNetworkName_throwsIllegalArgument() {
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
assertThrows(
IllegalArgumentException.class, () -> builder.setNetworkName("openthread-network"));
}
@Test
public void builder_setTooLongUtf8NetworkName_throwsIllegalArgument() {
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
// UTF-8 encoded length of "我的线程网络" is 18 bytes which exceeds the max length
assertThrows(IllegalArgumentException.class, () -> builder.setNetworkName("我的线程网络"));
}
@Test
public void builder_setValidUtf8NetworkName_success() {
ActiveOperationalDataset dataset =
new Builder(ActiveOperationalDataset.createRandomDataset())
.setNetworkName("我的网络")
.build();
assertThat(dataset.getNetworkName()).isEqualTo("我的网络");
}
@Test
public void builder_setValidPskc_success() {
byte[] pskc = base16().decode("A245479C836D551B9CA557F7B9D351B4");
ActiveOperationalDataset dataset =
new Builder(ActiveOperationalDataset.createRandomDataset()).setPskc(pskc).build();
assertThat(dataset.getPskc()).isEqualTo(pskc);
}
@Test
public void builder_setTooLongPskc_throwsIllegalArgument() {
byte[] tooLongPskc = base16().decode("A245479C836D551B9CA557F7B9D351B400");
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
assertThrows(IllegalArgumentException.class, () -> builder.setPskc(tooLongPskc));
}
@Test
public void builder_setValidChannelMask_success() {
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
SparseArray<byte[]> channelMask = new SparseArray<byte[]>(1);
channelMask.put(0, new byte[] {0x00, 0x00, 0x01, 0x00});
ActiveOperationalDataset dataset = builder.setChannelMask(channelMask).build();
SparseArray<byte[]> resultChannelMask = dataset.getChannelMask();
assertThat(resultChannelMask.size()).isEqualTo(1);
assertThat(resultChannelMask.get(0)).isEqualTo(new byte[] {0x00, 0x00, 0x01, 0x00});
}
@Test
public void builder_setEmptyChannelMask_throwsIllegalArgument() {
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
assertThrows(
IllegalArgumentException.class,
() -> builder.setChannelMask(new SparseArray<byte[]>()));
}
@Test
public void builder_setValidActiveTimestamp_success() {
ActiveOperationalDataset dataset =
new Builder(ActiveOperationalDataset.createRandomDataset())
.setActiveTimestamp(
new OperationalDatasetTimestamp(
/* seconds= */ 1,
/* ticks= */ 0,
/* isAuthoritativeSource= */ true))
.build();
assertThat(dataset.getActiveTimestamp().getSeconds()).isEqualTo(1);
assertThat(dataset.getActiveTimestamp().getTicks()).isEqualTo(0);
assertThat(dataset.getActiveTimestamp().isAuthoritativeSource()).isTrue();
}
@Test
public void builder_wrongMeshLocalPrefixLength_throwsIllegalArguments() {
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
// The Mesh-Local Prefix length must be 64 bits
assertThrows(
IllegalArgumentException.class,
() -> builder.setMeshLocalPrefix(new IpPrefix("fd00::/32")));
assertThrows(
IllegalArgumentException.class,
() -> builder.setMeshLocalPrefix(new IpPrefix("fd00::/96")));
// The Mesh-Local Prefix must start with 0xfd
assertThrows(
IllegalArgumentException.class,
() -> builder.setMeshLocalPrefix(new IpPrefix("fc00::/64")));
}
@Test
public void builder_meshLocalPrefixNotStartWith0xfd_throwsIllegalArguments() {
Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
assertThrows(
IllegalArgumentException.class,
() -> builder.setMeshLocalPrefix(new IpPrefix("fc00::/64")));
}
@Test
public void builder_setValidMeshLocalPrefix_success() {
ActiveOperationalDataset dataset =
new Builder(ActiveOperationalDataset.createRandomDataset())
.setMeshLocalPrefix(new IpPrefix("fd00::/64"))
.build();
assertThat(dataset.getMeshLocalPrefix()).isEqualTo(new IpPrefix("fd00::/64"));
}
@Test
public void builder_setValid1P2SecurityPolicy_success() {
ActiveOperationalDataset dataset =
new Builder(ActiveOperationalDataset.createRandomDataset())
.setSecurityPolicy(
new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}))
.build();
assertThat(dataset.getSecurityPolicy().getRotationTimeHours()).isEqualTo(672);
assertThat(dataset.getSecurityPolicy().getFlags())
.isEqualTo(new byte[] {(byte) 0xff, (byte) 0xf8});
}
@Test
public void builder_setValid1P1SecurityPolicy_success() {
ActiveOperationalDataset dataset =
new Builder(ActiveOperationalDataset.createRandomDataset())
.setSecurityPolicy(new SecurityPolicy(672, new byte[] {(byte) 0xff}))
.build();
assertThat(dataset.getSecurityPolicy().getRotationTimeHours()).isEqualTo(672);
assertThat(dataset.getSecurityPolicy().getFlags()).isEqualTo(new byte[] {(byte) 0xff});
}
@Test
public void securityPolicy_invalidRotationTime_throwsIllegalArguments() {
assertThrows(
IllegalArgumentException.class,
() -> new SecurityPolicy(0, new byte[] {(byte) 0xff, (byte) 0xf8}));
assertThrows(
IllegalArgumentException.class,
() -> new SecurityPolicy(0x1ffff, new byte[] {(byte) 0xff, (byte) 0xf8}));
}
@Test
public void securityPolicy_emptyFlags_throwsIllegalArguments() {
assertThrows(IllegalArgumentException.class, () -> new SecurityPolicy(672, new byte[] {}));
}
@Test
public void securityPolicy_tooLongFlags_success() {
SecurityPolicy securityPolicy =
new SecurityPolicy(672, new byte[] {0, 1, 2, 3, 4, 5, 6, 7});
assertThat(securityPolicy.getFlags()).isEqualTo(new byte[] {0, 1, 2, 3, 4, 5, 6, 7});
}
@Test
public void securityPolicy_equals() {
new EqualsTester()
.addEqualityGroup(
new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}),
new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}))
.addEqualityGroup(
new SecurityPolicy(1, new byte[] {(byte) 0xff}),
new SecurityPolicy(1, new byte[] {(byte) 0xff}))
.addEqualityGroup(
new SecurityPolicy(1, new byte[] {(byte) 0xff, (byte) 0xf8}),
new SecurityPolicy(1, new byte[] {(byte) 0xff, (byte) 0xf8}))
.testEquals();
}
}
/*
* 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.cts;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.net.thread.OperationalDatasetTimestamp;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.google.common.testing.EqualsTester;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Instant;
/** Tests for {@link OperationalDatasetTimestamp}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
public final class OperationalDatasetTimestampTest {
@Test
public void fromInstant_tooLargeInstant_throwsIllegalArgument() {
assertThrows(
IllegalArgumentException.class,
() ->
OperationalDatasetTimestamp.fromInstant(
Instant.ofEpochSecond(0xffffffffffffL + 1L)));
}
@Test
public void fromInstant_ticksIsRounded() {
Instant instant = Instant.ofEpochSecond(100L);
// 32767.5 / 32768 * 1000000000 = 999984741.2109375 and given the `ticks` is rounded, so
// the `ticks` should be 32767 for 999984741 and 0 (carried over to seconds) for 999984742.
OperationalDatasetTimestamp timestampTicks32767 =
OperationalDatasetTimestamp.fromInstant(instant.plusNanos(999984741));
OperationalDatasetTimestamp timestampTicks0 =
OperationalDatasetTimestamp.fromInstant(instant.plusNanos(999984742));
assertThat(timestampTicks32767.getSeconds()).isEqualTo(100L);
assertThat(timestampTicks0.getSeconds()).isEqualTo(101L);
assertThat(timestampTicks32767.getTicks()).isEqualTo(32767);
assertThat(timestampTicks0.getTicks()).isEqualTo(0);
assertThat(timestampTicks32767.isAuthoritativeSource()).isTrue();
assertThat(timestampTicks0.isAuthoritativeSource()).isTrue();
}
@Test
public void toInstant_nanosIsRounded() {
// 32767 / 32768 * 1000000000 = 999969482.421875
assertThat(new OperationalDatasetTimestamp(100L, 32767, false).toInstant().getNano())
.isEqualTo(999969482);
// 32766 / 32768 * 1000000000 = 999938964.84375
assertThat(new OperationalDatasetTimestamp(100L, 32766, false).toInstant().getNano())
.isEqualTo(999938965);
}
@Test
public void toInstant_onlyAuthoritativeSourceDiscarded() {
OperationalDatasetTimestamp timestamp1 =
new OperationalDatasetTimestamp(100L, 0x7fff, false);
OperationalDatasetTimestamp timestamp2 =
OperationalDatasetTimestamp.fromInstant(timestamp1.toInstant());
assertThat(timestamp2.getSeconds()).isEqualTo(100L);
assertThat(timestamp2.getTicks()).isEqualTo(0x7fff);
assertThat(timestamp2.isAuthoritativeSource()).isTrue();
}
@Test
public void constructor_tooLargeSeconds_throwsIllegalArguments() {
assertThrows(
IllegalArgumentException.class,
() ->
new OperationalDatasetTimestamp(
/* seconds= */ 0x0001112233445566L,
/* ticks= */ 0,
/* isAuthoritativeSource= */ true));
}
@Test
public void constructor_tooLargeTicks_throwsIllegalArguments() {
assertThrows(
IllegalArgumentException.class,
() ->
new OperationalDatasetTimestamp(
/* seconds= */ 0x01L,
/* ticks= */ 0x8000,
/* isAuthoritativeSource= */ true));
}
@Test
public void equalityTests() {
new EqualsTester()
.addEqualityGroup(
new OperationalDatasetTimestamp(100, 100, false),
new OperationalDatasetTimestamp(100, 100, false))
.addEqualityGroup(
new OperationalDatasetTimestamp(0, 0, false),
new OperationalDatasetTimestamp(0, 0, false))
.addEqualityGroup(
new OperationalDatasetTimestamp(0xffffffffffffL, 0x7fff, true),
new OperationalDatasetTimestamp(0xffffffffffffL, 0x7fff, true))
.testEquals();
}
}
/*
* 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.cts;
import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.net.IpPrefix;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.OperationalDatasetTimestamp;
import android.net.thread.PendingOperationalDataset;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.google.common.primitives.Bytes;
import com.google.common.testing.EqualsTester;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
/** Tests for {@link PendingOperationalDataset}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
public final class PendingOperationalDatasetTest {
private static final ActiveOperationalDataset DEFAULT_ACTIVE_DATASET =
ActiveOperationalDataset.createRandomDataset();
@Test
public void parcelable_parcelingIsLossLess() {
PendingOperationalDataset dataset =
new PendingOperationalDataset(
DEFAULT_ACTIVE_DATASET,
new OperationalDatasetTimestamp(31536000, 200, false),
Duration.ofHours(100));
assertParcelingIsLossless(dataset);
}
@Test
public void equalityTests() {
ActiveOperationalDataset activeDataset1 = ActiveOperationalDataset.createRandomDataset();
ActiveOperationalDataset activeDataset2 = ActiveOperationalDataset.createRandomDataset();
new EqualsTester()
.addEqualityGroup(
new PendingOperationalDataset(
activeDataset1,
new OperationalDatasetTimestamp(31536000, 100, false),
Duration.ofMillis(0)),
new PendingOperationalDataset(
activeDataset1,
new OperationalDatasetTimestamp(31536000, 100, false),
Duration.ofMillis(0)))
.addEqualityGroup(
new PendingOperationalDataset(
activeDataset2,
new OperationalDatasetTimestamp(31536000, 100, false),
Duration.ofMillis(0)),
new PendingOperationalDataset(
activeDataset2,
new OperationalDatasetTimestamp(31536000, 100, false),
Duration.ofMillis(0)))
.addEqualityGroup(
new PendingOperationalDataset(
activeDataset2,
new OperationalDatasetTimestamp(15768000, 0, false),
Duration.ofMillis(0)),
new PendingOperationalDataset(
activeDataset2,
new OperationalDatasetTimestamp(15768000, 0, false),
Duration.ofMillis(0)))
.addEqualityGroup(
new PendingOperationalDataset(
activeDataset2,
new OperationalDatasetTimestamp(15768000, 0, false),
Duration.ofMillis(100)),
new PendingOperationalDataset(
activeDataset2,
new OperationalDatasetTimestamp(15768000, 0, false),
Duration.ofMillis(100)))
.testEquals();
}
@Test
public void constructor_correctValuesAreSet() {
PendingOperationalDataset dataset =
new PendingOperationalDataset(
DEFAULT_ACTIVE_DATASET,
new OperationalDatasetTimestamp(31536000, 200, false),
Duration.ofHours(100));
assertThat(dataset.getActiveOperationalDataset()).isEqualTo(DEFAULT_ACTIVE_DATASET);
assertThat(dataset.getPendingTimestamp())
.isEqualTo(new OperationalDatasetTimestamp(31536000, 200, false));
assertThat(dataset.getDelayTimer()).isEqualTo(Duration.ofHours(100));
}
@Test
public void fromThreadTlvs_openthreadTlvs_success() {
// An example Pending Operational Dataset which is generated with OpenThread CLI:
// Pending Timestamp: 2
// Active Timestamp: 1
// Channel: 26
// Channel Mask: 0x07fff800
// Delay: 46354
// Ext PAN ID: a74182f4d3f4de41
// Mesh Local Prefix: fd46:c1b9:e159:5574::/64
// Network Key: ed916e454d96fd00184f10a6f5c9e1d3
// Network Name: OpenThread-bff8
// PAN ID: 0xbff8
// PSKc: 264f78414adc683191863d968f72d1b7
// Security Policy: 672 onrc
final byte[] OPENTHREAD_PENDING_DATASET_TLVS =
base16().lowerCase()
.decode(
"0e0800000000000100003308000000000002000034040000b51200030000"
+ "1a35060004001fffe00208a74182f4d3f4de410708fd46c1b9"
+ "e15955740510ed916e454d96fd00184f10a6f5c9e1d3030f4f"
+ "70656e5468726561642d626666380102bff80410264f78414a"
+ "dc683191863d968f72d1b70c0402a0f7f8");
PendingOperationalDataset pendingDataset =
PendingOperationalDataset.fromThreadTlvs(OPENTHREAD_PENDING_DATASET_TLVS);
ActiveOperationalDataset activeDataset = pendingDataset.getActiveOperationalDataset();
assertThat(pendingDataset.getPendingTimestamp().getSeconds()).isEqualTo(2L);
assertThat(activeDataset.getActiveTimestamp().getSeconds()).isEqualTo(1L);
assertThat(activeDataset.getChannel()).isEqualTo(26);
assertThat(activeDataset.getChannelMask().get(0))
.isEqualTo(new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0});
assertThat(pendingDataset.getDelayTimer().toMillis()).isEqualTo(46354);
assertThat(activeDataset.getExtendedPanId())
.isEqualTo(base16().lowerCase().decode("a74182f4d3f4de41"));
assertThat(activeDataset.getMeshLocalPrefix())
.isEqualTo(new IpPrefix("fd46:c1b9:e159:5574::/64"));
assertThat(activeDataset.getNetworkKey())
.isEqualTo(base16().lowerCase().decode("ed916e454d96fd00184f10a6f5c9e1d3"));
assertThat(activeDataset.getNetworkName()).isEqualTo("OpenThread-bff8");
assertThat(activeDataset.getPanId()).isEqualTo(0xbff8);
assertThat(activeDataset.getPskc())
.isEqualTo(base16().lowerCase().decode("264f78414adc683191863d968f72d1b7"));
assertThat(activeDataset.getSecurityPolicy().getRotationTimeHours()).isEqualTo(672);
assertThat(activeDataset.getSecurityPolicy().getFlags())
.isEqualTo(new byte[] {(byte) 0xf7, (byte) 0xf8});
}
@Test
public void fromThreadTlvs_completePendingDatasetTlvs_success() {
// Type Length Value
// 0x33 0x08 0x0000000000010000 (Pending Timestamp TLV)
// 0x34 0x04 0x0000012C (Delay Timer TLV)
final byte[] pendingTimestampAndDelayTimerTlvs =
base16().decode("3308000000000001000034040000012C");
final byte[] pendingDatasetTlvs =
Bytes.concat(
pendingTimestampAndDelayTimerTlvs, DEFAULT_ACTIVE_DATASET.toThreadTlvs());
PendingOperationalDataset dataset =
PendingOperationalDataset.fromThreadTlvs(pendingDatasetTlvs);
assertThat(dataset.getActiveOperationalDataset()).isEqualTo(DEFAULT_ACTIVE_DATASET);
assertThat(dataset.getPendingTimestamp())
.isEqualTo(new OperationalDatasetTimestamp(1, 0, false));
assertThat(dataset.getDelayTimer()).isEqualTo(Duration.ofMillis(300));
}
@Test
public void fromThreadTlvs_PendingTimestampTlvIsMissing_throwsIllegalArgument() {
// Type Length Value
// 0x34 0x04 0x00000064 (Delay Timer TLV)
final byte[] pendingTimestampAndDelayTimerTlvs = base16().decode("34040000012C");
final byte[] pendingDatasetTlvs =
Bytes.concat(
pendingTimestampAndDelayTimerTlvs, DEFAULT_ACTIVE_DATASET.toThreadTlvs());
assertThrows(
IllegalArgumentException.class,
() -> PendingOperationalDataset.fromThreadTlvs(pendingDatasetTlvs));
}
@Test
public void fromThreadTlvs_delayTimerTlvIsMissing_throwsIllegalArgument() {
// Type Length Value
// 0x33 0x08 0x0000000000010000 (Pending Timestamp TLV)
final byte[] pendingTimestampAndDelayTimerTlvs = base16().decode("33080000000000010000");
final byte[] pendingDatasetTlvs =
Bytes.concat(
pendingTimestampAndDelayTimerTlvs, DEFAULT_ACTIVE_DATASET.toThreadTlvs());
assertThrows(
IllegalArgumentException.class,
() -> PendingOperationalDataset.fromThreadTlvs(pendingDatasetTlvs));
}
@Test
public void fromThreadTlvs_activeDatasetTlvs_throwsIllegalArgument() {
final byte[] activeDatasetTlvs = DEFAULT_ACTIVE_DATASET.toThreadTlvs();
assertThrows(
IllegalArgumentException.class,
() -> PendingOperationalDataset.fromThreadTlvs(activeDatasetTlvs));
}
@Test
public void fromThreadTlvs_malformedTlvs_throwsIllegalArgument() {
final byte[] invalidTlvs = new byte[] {0x00};
assertThrows(
IllegalArgumentException.class,
() -> PendingOperationalDataset.fromThreadTlvs(invalidTlvs));
}
@Test
public void toThreadTlvs_conversionIsLossLess() {
PendingOperationalDataset dataset1 =
new PendingOperationalDataset(
DEFAULT_ACTIVE_DATASET,
new OperationalDatasetTimestamp(31536000, 200, false),
Duration.ofHours(100));
PendingOperationalDataset dataset2 =
PendingOperationalDataset.fromThreadTlvs(dataset1.toThreadTlvs());
assertThat(dataset2).isEqualTo(dataset1);
}
}
//
// 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",
}
<?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>
<?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>
/*
* 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);
}
}
/*
* 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);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment