/* * 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. * *

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. * * @see ThreadNetworkController#scheduleMigration * @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 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. * * @param activeOpDataset the included Active Operational Dataset * @param pendingTimestamp the Pending Timestamp which represents the version of this Pending * Dataset * @param delayTimer the delay after when {@code activeOpDataset} will be committed on this * device; use {@link Duration#ZERO} to tell the system to choose a reasonable value * automatically */ 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. * *

{@code tlvs} can be obtained from the value of a Thread Pending Operational Dataset TLV * (see the Thread * specification 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 newUnknownTlvs = new SparseArray<>(); OperationalDatasetTimestamp pendingTimestamp = null; Duration delayTimer = null; ActiveOperationalDataset activeDataset = ActiveOperationalDataset.fromThreadTlvs(tlvs); SparseArray 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. * *

See the Thread * specification 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()); } }