Merge "Add mulit devices mdns tests" into main
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index 2f675a9..d8cccb2 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -180,8 +180,18 @@
return new ArrayList<>(mHostAddresses);
}
- /** Set the host addresses */
+ /**
+ * Set the host addresses.
+ *
+ * <p>When registering hosts/services, there can only be one registration including address
+ * records for a given hostname.
+ *
+ * <p>For example, if a client registers a service with the hostname "MyHost" and the address
+ * records of 192.168.1.1 and 192.168.1.2, then other registrations for the hostname "MyHost"
+ * must not have any address record, otherwise there will be a conflict.
+ */
public void setHostAddresses(@NonNull List<InetAddress> addresses) {
+ // TODO: b/284905335 - Notify the client when there is a conflict.
mHostAddresses.clear();
mHostAddresses.addAll(addresses);
}
diff --git a/thread/framework/java/android/net/thread/IConfigurationReceiver.aidl b/thread/framework/java/android/net/thread/IConfigurationReceiver.aidl
new file mode 100644
index 0000000..dcc4545
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IConfigurationReceiver.aidl
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import android.net.thread.ThreadConfiguration;
+
+/** Receives the result of a Thread Configuration change. @hide */
+oneway interface IConfigurationReceiver {
+ void onConfigurationChanged(in ThreadConfiguration configuration);
+}
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
index c5ca557..f50de74 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -19,11 +19,13 @@
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.ChannelMaxPower;
import android.net.thread.IActiveOperationalDatasetReceiver;
-import android.net.thread.IOperationalDatasetCallback;
+import android.net.thread.IConfigurationReceiver;
import android.net.thread.IOperationReceiver;
+import android.net.thread.IOperationalDatasetCallback;
import android.net.thread.IScheduleMigrationReceiver;
import android.net.thread.IStateCallback;
import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadConfiguration;
/**
* Interface for communicating with ThreadNetworkControllerService.
@@ -46,4 +48,7 @@
void createRandomizedDataset(String networkName, IActiveOperationalDatasetReceiver receiver);
void setEnabled(boolean enabled, in IOperationReceiver receiver);
+ void setConfiguration(in ThreadConfiguration config, in IOperationReceiver receiver);
+ void registerConfigurationCallback(in IConfigurationReceiver receiver);
+ void unregisterConfigurationCallback(in IConfigurationReceiver receiver);
}
diff --git a/thread/framework/java/android/net/thread/ThreadConfiguration.aidl b/thread/framework/java/android/net/thread/ThreadConfiguration.aidl
new file mode 100644
index 0000000..9473411
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadConfiguration.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+parcelable ThreadConfiguration;
diff --git a/thread/framework/java/android/net/thread/ThreadConfiguration.java b/thread/framework/java/android/net/thread/ThreadConfiguration.java
new file mode 100644
index 0000000..e09b3a6
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadConfiguration.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.thread;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Data interface for Thread device configuration.
+ *
+ * <p>An example usage of creating a {@link ThreadConfiguration} that turns on NAT64 feature based
+ * on an existing {@link ThreadConfiguration}:
+ *
+ * <pre>{@code
+ * ThreadConfiguration config =
+ * new ThreadConfiguration.Builder(existingConfig).setNat64Enabled(true).build();
+ * }</pre>
+ *
+ * @see ThreadNetworkController#setConfiguration
+ * @see ThreadNetworkController#registerConfigurationCallback
+ * @see ThreadNetworkController#unregisterConfigurationCallback
+ * @hide
+ */
+// @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
+// @SystemApi
+public final class ThreadConfiguration implements Parcelable {
+ private final boolean mNat64Enabled;
+ private final boolean mDhcp6PdEnabled;
+
+ private ThreadConfiguration(Builder builder) {
+ this(builder.mNat64Enabled, builder.mDhcp6PdEnabled);
+ }
+
+ private ThreadConfiguration(boolean nat64Enabled, boolean dhcp6PdEnabled) {
+ this.mNat64Enabled = nat64Enabled;
+ this.mDhcp6PdEnabled = dhcp6PdEnabled;
+ }
+
+ /** Returns {@code true} if NAT64 is enabled. */
+ public boolean isNat64Enabled() {
+ return mNat64Enabled;
+ }
+
+ /** Returns {@code true} if DHCPv6 Prefix Delegation is enabled. */
+ public boolean isDhcp6PdEnabled() {
+ return mDhcp6PdEnabled;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof ThreadConfiguration)) {
+ return false;
+ } else {
+ ThreadConfiguration otherConfig = (ThreadConfiguration) other;
+ return mNat64Enabled == otherConfig.mNat64Enabled
+ && mDhcp6PdEnabled == otherConfig.mDhcp6PdEnabled;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mNat64Enabled, mDhcp6PdEnabled);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append('{');
+ sb.append("Nat64Enabled=").append(mNat64Enabled);
+ sb.append(", Dhcp6PdEnabled=").append(mDhcp6PdEnabled);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeBoolean(mNat64Enabled);
+ dest.writeBoolean(mDhcp6PdEnabled);
+ }
+
+ public static final @NonNull Creator<ThreadConfiguration> CREATOR =
+ new Creator<>() {
+ @Override
+ public ThreadConfiguration createFromParcel(Parcel in) {
+ ThreadConfiguration.Builder builder = new ThreadConfiguration.Builder();
+ builder.setNat64Enabled(in.readBoolean());
+ builder.setDhcp6PdEnabled(in.readBoolean());
+ return builder.build();
+ }
+
+ @Override
+ public ThreadConfiguration[] newArray(int size) {
+ return new ThreadConfiguration[size];
+ }
+ };
+
+ /** The builder for creating {@link ThreadConfiguration} objects. */
+ public static final class Builder {
+ private boolean mNat64Enabled = false;
+ private boolean mDhcp6PdEnabled = false;
+
+ /** Creates a new {@link Builder} object with all features disabled. */
+ public Builder() {}
+
+ /**
+ * Creates a new {@link Builder} object from a {@link ThreadConfiguration} object.
+ *
+ * @param config the Border Router configurations to be copied
+ */
+ public Builder(@NonNull ThreadConfiguration config) {
+ Objects.requireNonNull(config);
+
+ mNat64Enabled = config.mNat64Enabled;
+ mDhcp6PdEnabled = config.mDhcp6PdEnabled;
+ }
+
+ /**
+ * Enables or disables NAT64 for the device.
+ *
+ * <p>Enabling this feature will allow Thread devices to connect to the internet/cloud over
+ * IPv4.
+ */
+ @NonNull
+ public Builder setNat64Enabled(boolean enabled) {
+ this.mNat64Enabled = enabled;
+ return this;
+ }
+
+ /**
+ * Enables or disables Prefix Delegation for the device.
+ *
+ * <p>Enabling this feature will allow Thread devices to connect to the internet/cloud over
+ * IPv6.
+ */
+ @NonNull
+ public Builder setDhcp6PdEnabled(boolean enabled) {
+ this.mDhcp6PdEnabled = enabled;
+ return this;
+ }
+
+ /** Creates a new {@link ThreadConfiguration} object. */
+ @NonNull
+ public ThreadConfiguration build() {
+ return new ThreadConfiguration(this);
+ }
+ }
+}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index 8d6b40a..30b3d6a 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -41,6 +41,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
+import java.util.function.Consumer;
/**
* Provides the primary APIs for controlling all aspects of a Thread network.
@@ -124,6 +125,12 @@
private final Map<OperationalDatasetCallback, OperationalDatasetCallbackProxy>
mOpDatasetCallbackMap = new HashMap<>();
+ private final Object mConfigurationCallbackMapLock = new Object();
+
+ @GuardedBy("mConfigurationCallbackMapLock")
+ private final Map<Consumer<ThreadConfiguration>, ConfigurationCallbackProxy>
+ mConfigurationCallbackMap = new HashMap<>();
+
/** @hide */
public ThreadNetworkController(@NonNull IThreadNetworkController controllerService) {
requireNonNull(controllerService, "controllerService cannot be null");
@@ -579,6 +586,97 @@
}
/**
+ * Configures the Thread features for this device.
+ *
+ * <p>This method sets the {@link ThreadConfiguration} for this device. On success, the {@link
+ * OutcomeReceiver#onResult} will be called, and the {@code configuration} will be applied and
+ * persisted to the device; the configuration changes can be observed by {@link
+ * #registerConfigurationCallback}. On failure, {@link OutcomeReceiver#onError} of {@code
+ * receiver} will be invoked with a specific error.
+ *
+ * @param configuration the configuration to set
+ * @param executor the executor to execute {@code receiver}
+ * @param receiver the receiver to receive result of this operation
+ * @hide
+ */
+ // @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
+ // @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
+ public void setConfiguration(
+ @NonNull ThreadConfiguration configuration,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ requireNonNull(configuration, "Configuration cannot be null");
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(receiver, "receiver cannot be null");
+ try {
+ mControllerService.setConfiguration(
+ configuration, new OperationReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Registers a callback to be called when the configuration is changed.
+ *
+ * <p>Upon return of this method, {@code callback} will be invoked immediately with the new
+ * {@link ThreadConfiguration}.
+ *
+ * @param executor the executor to execute the {@code callback}
+ * @param callback the callback to receive Thread configuration changes
+ * @throws IllegalArgumentException if {@code callback} has already been registered
+ * @hide
+ */
+ // @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
+ // @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
+ public void registerConfigurationCallback(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<ThreadConfiguration> callback) {
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(callback, "callback cannot be null");
+ synchronized (mConfigurationCallbackMapLock) {
+ if (mConfigurationCallbackMap.containsKey(callback)) {
+ throw new IllegalArgumentException("callback has already been registered");
+ }
+ ConfigurationCallbackProxy callbackProxy =
+ new ConfigurationCallbackProxy(executor, callback);
+ mConfigurationCallbackMap.put(callback, callbackProxy);
+ try {
+ mControllerService.registerConfigurationCallback(callbackProxy);
+ } catch (RemoteException e) {
+ mConfigurationCallbackMap.remove(callback);
+ e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Unregisters the configuration callback.
+ *
+ * @param callback the callback which has been registered with {@link
+ * #registerConfigurationCallback}
+ * @throws IllegalArgumentException if {@code callback} hasn't been registered
+ * @hide
+ */
+ // @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
+ // @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
+ public void unregisterConfigurationCallback(@NonNull Consumer<ThreadConfiguration> callback) {
+ requireNonNull(callback, "callback cannot be null");
+ synchronized (mConfigurationCallbackMapLock) {
+ ConfigurationCallbackProxy callbackProxy = mConfigurationCallbackMap.get(callback);
+ if (callbackProxy == null) {
+ throw new IllegalArgumentException("callback hasn't been registered");
+ }
+ try {
+ mControllerService.unregisterConfigurationCallback(callbackProxy);
+ mConfigurationCallbackMap.remove(callbackProxy.mConfigurationConsumer);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
* Sets to use a specified test network as the upstream.
*
* @param testNetworkInterfaceName The name of the test network interface. When it's null,
@@ -764,4 +862,26 @@
propagateError(mExecutor, mResultReceiver, errorCode, errorMessage);
}
}
+
+ private static final class ConfigurationCallbackProxy extends IConfigurationReceiver.Stub {
+ final Executor mExecutor;
+ final Consumer<ThreadConfiguration> mConfigurationConsumer;
+
+ ConfigurationCallbackProxy(
+ @CallbackExecutor Executor executor,
+ Consumer<ThreadConfiguration> ConfigurationConsumer) {
+ this.mExecutor = executor;
+ this.mConfigurationConsumer = ConfigurationConsumer;
+ }
+
+ @Override
+ public void onConfigurationChanged(ThreadConfiguration configuration) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mConfigurationConsumer.accept(configuration));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ }
}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkFlags.java b/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
index e6ab988..691bbf5 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
@@ -27,5 +27,9 @@
/** @hide */
public static final String FLAG_THREAD_ENABLED = "com.android.net.thread.flags.thread_enabled";
+ /** @hide */
+ public static final String FLAG_CONFIGURATION_ENABLED =
+ "com.android.net.thread.flags.configuration_enabled";
+
private ThreadNetworkFlags() {}
}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 0c77dee..1d823e0 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -91,12 +91,14 @@
import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
import android.net.thread.ChannelMaxPower;
import android.net.thread.IActiveOperationalDatasetReceiver;
+import android.net.thread.IConfigurationReceiver;
import android.net.thread.IOperationReceiver;
import android.net.thread.IOperationalDatasetCallback;
import android.net.thread.IStateCallback;
import android.net.thread.IThreadNetworkController;
import android.net.thread.OperationalDatasetTimestamp;
import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadConfiguration;
import android.net.thread.ThreadNetworkController;
import android.net.thread.ThreadNetworkController.DeviceRole;
import android.net.thread.ThreadNetworkException;
@@ -189,6 +191,8 @@
private final OtDaemonCallbackProxy mOtDaemonCallbackProxy = new OtDaemonCallbackProxy();
private final ConnectivityResources mResources;
private final Supplier<String> mCountryCodeSupplier;
+ private final Map<IConfigurationReceiver, IBinder.DeathRecipient> mConfigurationReceivers =
+ new HashMap<>();
// This should not be directly used for calling IOtDaemon APIs because ot-daemon may die and
// {@code mOtDaemon} will be set to {@code null}. Instead, use {@code getOtDaemon()}
@@ -518,17 +522,86 @@
}
}
+ @Override
+ public void setConfiguration(
+ @NonNull ThreadConfiguration configuration, @NonNull IOperationReceiver receiver) {
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ mHandler.post(() -> setConfigurationInternal(configuration, receiver));
+ }
+
+ private void setConfigurationInternal(
+ @NonNull ThreadConfiguration configuration,
+ @NonNull IOperationReceiver operationReceiver) {
+ checkOnHandlerThread();
+
+ Log.i(TAG, "Set Thread configuration: " + configuration);
+
+ final boolean changed = mPersistentSettings.putConfiguration(configuration);
+ try {
+ operationReceiver.onSuccess();
+ } catch (RemoteException e) {
+ // do nothing if the client is dead
+ }
+ if (changed) {
+ for (IConfigurationReceiver configReceiver : mConfigurationReceivers.keySet()) {
+ try {
+ configReceiver.onConfigurationChanged(configuration);
+ } catch (RemoteException e) {
+ // do nothing if the client is dead
+ }
+ }
+ }
+ }
+
+ @Override
+ public void registerConfigurationCallback(@NonNull IConfigurationReceiver callback) {
+ enforceAllPermissionsGranted(permission.THREAD_NETWORK_PRIVILEGED);
+ mHandler.post(() -> registerConfigurationCallbackInternal(callback));
+ }
+
+ private void registerConfigurationCallbackInternal(@NonNull IConfigurationReceiver callback) {
+ checkOnHandlerThread();
+ if (mConfigurationReceivers.containsKey(callback)) {
+ throw new IllegalStateException("Registering the same IConfigurationReceiver twice");
+ }
+ IBinder.DeathRecipient deathRecipient =
+ () -> mHandler.post(() -> unregisterConfigurationCallbackInternal(callback));
+ try {
+ callback.asBinder().linkToDeath(deathRecipient, 0);
+ } catch (RemoteException e) {
+ return;
+ }
+ mConfigurationReceivers.put(callback, deathRecipient);
+ try {
+ callback.onConfigurationChanged(mPersistentSettings.getConfiguration());
+ } catch (RemoteException e) {
+ // do nothing if the client is dead
+ }
+ }
+
+ @Override
+ public void unregisterConfigurationCallback(@NonNull IConfigurationReceiver callback) {
+ enforceAllPermissionsGranted(permission.THREAD_NETWORK_PRIVILEGED);
+ mHandler.post(() -> unregisterConfigurationCallbackInternal(callback));
+ }
+
+ private void unregisterConfigurationCallbackInternal(@NonNull IConfigurationReceiver callback) {
+ checkOnHandlerThread();
+ if (!mConfigurationReceivers.containsKey(callback)) {
+ return;
+ }
+ callback.asBinder().unlinkToDeath(mConfigurationReceivers.remove(callback), 0);
+ }
+
private void registerUserRestrictionsReceiver() {
mContext.registerReceiver(
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- onUserRestrictionsChanged(isThreadUserRestricted());
+ mHandler.post(() -> onUserRestrictionsChanged(isThreadUserRestricted()));
}
},
- new IntentFilter(UserManager.ACTION_USER_RESTRICTIONS_CHANGED),
- null /* broadcastPermission */,
- mHandler);
+ new IntentFilter(UserManager.ACTION_USER_RESTRICTIONS_CHANGED));
}
private void onUserRestrictionsChanged(boolean newUserRestrictedState) {
@@ -580,12 +653,10 @@
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- onAirplaneModeChanged(isAirplaneModeOn());
+ mHandler.post(() -> onAirplaneModeChanged(isAirplaneModeOn()));
}
},
- new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED),
- null /* broadcastPermission */,
- mHandler);
+ new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED));
}
private void onAirplaneModeChanged(boolean newAirplaneModeOn) {
diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
index f18aac9..747cc96 100644
--- a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -18,9 +18,11 @@
import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ApexEnvironment;
import android.content.Context;
+import android.net.thread.ThreadConfiguration;
import android.os.PersistableBundle;
import android.util.AtomicFile;
import android.util.Log;
@@ -74,6 +76,16 @@
/** Stores the Thread country code, null if no country code is stored. */
public static final Key<String> THREAD_COUNTRY_CODE = new Key<>("thread_country_code", null);
+ /** Stores the Thread NAT64 feature toggle state, true for enabled and false for disabled. */
+ private static final Key<Boolean> CONFIG_NAT64_ENABLED =
+ new Key<>("config_nat64_enabled", false);
+
+ /**
+ * Stores the Thread DHCPv6-PD feature toggle state, true for enabled and false for disabled.
+ */
+ private static final Key<Boolean> CONFIG_DHCP6_PD_ENABLED =
+ new Key<>("config_dhcp6_pd_enabled", false);
+
/******** Thread persistent setting keys ***************/
@GuardedBy("mLock")
@@ -175,6 +187,30 @@
}
/**
+ * Store a {@link ThreadConfiguration} to the persistent settings.
+ *
+ * @param configuration {@link ThreadConfiguration} to be stored.
+ * @return {@code true} if the configuration was changed, {@code false} otherwise.
+ */
+ public boolean putConfiguration(@NonNull ThreadConfiguration configuration) {
+ if (getConfiguration().equals(configuration)) {
+ return false;
+ }
+ putObject(CONFIG_NAT64_ENABLED.key, configuration.isNat64Enabled());
+ putObject(CONFIG_DHCP6_PD_ENABLED.key, configuration.isDhcp6PdEnabled());
+ writeToStoreFile();
+ return true;
+ }
+
+ /** Retrieve the {@link ThreadConfiguration} from the persistent settings. */
+ public ThreadConfiguration getConfiguration() {
+ return new ThreadConfiguration.Builder()
+ .setNat64Enabled(get(CONFIG_NAT64_ENABLED))
+ .setDhcp6PdEnabled(get(CONFIG_DHCP6_PD_ENABLED))
+ .build();
+ }
+
+ /**
* Base class to store string key and its default value.
*
* @param <T> Type of the value.
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index 0e95703..41f34ff 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -45,6 +45,7 @@
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
+
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context;
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index 2f58943..de6d1e1 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -59,6 +59,7 @@
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.IActiveOperationalDatasetReceiver;
import android.net.thread.IOperationReceiver;
+import android.net.thread.ThreadConfiguration;
import android.net.thread.ThreadNetworkException;
import android.os.Handler;
import android.os.IBinder;
@@ -539,9 +540,7 @@
.when(mContext)
.registerReceiver(
any(BroadcastReceiver.class),
- argThat(actualIntentFilter -> actualIntentFilter.hasAction(action)),
- any(),
- any());
+ argThat(actualIntentFilter -> actualIntentFilter.hasAction(action)));
return receiverRef;
}
@@ -708,4 +707,35 @@
inOrder.verify(mMockTunIfController, times(1)).setInterfaceUp(false);
inOrder.verify(mMockTunIfController, times(1)).setInterfaceUp(true);
}
+
+ @Test
+ public void setConfiguration_configurationUpdated() throws Exception {
+ mService.initialize();
+ final IOperationReceiver mockReceiver1 = mock(IOperationReceiver.class);
+ final IOperationReceiver mockReceiver2 = mock(IOperationReceiver.class);
+ final IOperationReceiver mockReceiver3 = mock(IOperationReceiver.class);
+ ThreadConfiguration config1 =
+ new ThreadConfiguration.Builder()
+ .setNat64Enabled(false)
+ .setDhcp6PdEnabled(false)
+ .build();
+ ThreadConfiguration config2 =
+ new ThreadConfiguration.Builder()
+ .setNat64Enabled(true)
+ .setDhcp6PdEnabled(true)
+ .build();
+ ThreadConfiguration config3 =
+ new ThreadConfiguration.Builder(config2).build(); // Same as config2
+
+ mService.setConfiguration(config1, mockReceiver1);
+ mService.setConfiguration(config2, mockReceiver2);
+ mService.setConfiguration(config3, mockReceiver3);
+ mTestLooper.dispatchAll();
+
+ assertThat(mPersistentSettings.getConfiguration()).isEqualTo(config3);
+ InOrder inOrder = Mockito.inOrder(mockReceiver1, mockReceiver2, mockReceiver3);
+ inOrder.verify(mockReceiver1).onSuccess();
+ inOrder.verify(mockReceiver2).onSuccess();
+ inOrder.verify(mockReceiver3).onSuccess();
+ }
}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
index 7d2fe91..c932ac8 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
@@ -21,16 +21,12 @@
import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyInt;
-import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.validateMockitoUsage;
-import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.res.Resources;
+import android.net.thread.ThreadConfiguration;
import android.os.PersistableBundle;
import android.util.AtomicFile;
@@ -42,13 +38,14 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.io.ByteArrayOutputStream;
-import java.io.FileInputStream;
import java.io.FileOutputStream;
/** Unit tests for {@link ThreadPersistentSettings}. */
@@ -57,12 +54,15 @@
public class ThreadPersistentSettingsTest {
private static final String TEST_COUNTRY_CODE = "CN";
- @Mock private AtomicFile mAtomicFile;
@Mock Resources mResources;
@Mock ConnectivityResources mConnectivityResources;
+ private AtomicFile mAtomicFile;
private ThreadPersistentSettings mThreadPersistentSettings;
+ @Rule(order = 0)
+ public final TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
@@ -70,8 +70,7 @@
when(mConnectivityResources.get()).thenReturn(mResources);
when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(true);
- FileOutputStream fos = mock(FileOutputStream.class);
- when(mAtomicFile.startWrite()).thenReturn(fos);
+ mAtomicFile = createAtomicFile();
mThreadPersistentSettings =
new ThreadPersistentSettings(mAtomicFile, mConnectivityResources);
}
@@ -85,7 +84,7 @@
@Test
public void initialize_readsFromFile() throws Exception {
byte[] data = createXmlForParsing(THREAD_ENABLED.key, false);
- setupAtomicFileMockForRead(data);
+ setupAtomicFileForRead(data);
mThreadPersistentSettings.initialize();
@@ -95,7 +94,7 @@
@Test
public void initialize_ThreadDisabledInResources_returnsThreadDisabled() throws Exception {
when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(false);
- setupAtomicFileMockForRead(new byte[0]);
+ setupAtomicFileForRead(new byte[0]);
mThreadPersistentSettings.initialize();
@@ -107,7 +106,7 @@
throws Exception {
when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(false);
byte[] data = createXmlForParsing(THREAD_ENABLED.key, true);
- setupAtomicFileMockForRead(data);
+ setupAtomicFileForRead(data);
mThreadPersistentSettings.initialize();
@@ -119,9 +118,6 @@
mThreadPersistentSettings.put(THREAD_ENABLED.key, true);
assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isTrue();
- // Confirm that file writes have been triggered.
- verify(mAtomicFile).startWrite();
- verify(mAtomicFile).finishWrite(any());
}
@Test
@@ -129,9 +125,8 @@
mThreadPersistentSettings.put(THREAD_ENABLED.key, false);
assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
- // Confirm that file writes have been triggered.
- verify(mAtomicFile).startWrite();
- verify(mAtomicFile).finishWrite(any());
+ mThreadPersistentSettings.initialize();
+ assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
}
@Test
@@ -139,10 +134,8 @@
mThreadPersistentSettings.put(THREAD_COUNTRY_CODE.key, TEST_COUNTRY_CODE);
assertThat(mThreadPersistentSettings.get(THREAD_COUNTRY_CODE)).isEqualTo(TEST_COUNTRY_CODE);
-
- // Confirm that file writes have been triggered.
- verify(mAtomicFile).startWrite();
- verify(mAtomicFile).finishWrite(any());
+ mThreadPersistentSettings.initialize();
+ assertThat(mThreadPersistentSettings.get(THREAD_COUNTRY_CODE)).isEqualTo(TEST_COUNTRY_CODE);
}
@Test
@@ -150,10 +143,63 @@
mThreadPersistentSettings.put(THREAD_COUNTRY_CODE.key, null);
assertThat(mThreadPersistentSettings.get(THREAD_COUNTRY_CODE)).isNull();
+ mThreadPersistentSettings.initialize();
+ assertThat(mThreadPersistentSettings.get(THREAD_COUNTRY_CODE)).isNull();
+ }
- // Confirm that file writes have been triggered.
- verify(mAtomicFile).startWrite();
- verify(mAtomicFile).finishWrite(any());
+ @Test
+ public void putConfiguration_sameValues_returnsFalse() {
+ ThreadConfiguration configuration =
+ new ThreadConfiguration.Builder()
+ .setNat64Enabled(true)
+ .setDhcp6PdEnabled(true)
+ .build();
+ mThreadPersistentSettings.putConfiguration(configuration);
+
+ assertThat(mThreadPersistentSettings.putConfiguration(configuration)).isFalse();
+ }
+
+ @Test
+ public void putConfiguration_differentValues_returnsTrue() {
+ ThreadConfiguration configuration1 =
+ new ThreadConfiguration.Builder()
+ .setNat64Enabled(false)
+ .setDhcp6PdEnabled(false)
+ .build();
+ mThreadPersistentSettings.putConfiguration(configuration1);
+ ThreadConfiguration configuration2 =
+ new ThreadConfiguration.Builder()
+ .setNat64Enabled(true)
+ .setDhcp6PdEnabled(true)
+ .build();
+
+ assertThat(mThreadPersistentSettings.putConfiguration(configuration2)).isTrue();
+ }
+
+ @Test
+ public void putConfiguration_nat64Enabled_valuesUpdatedAndPersisted() throws Exception {
+ ThreadConfiguration configuration =
+ new ThreadConfiguration.Builder().setNat64Enabled(true).build();
+ mThreadPersistentSettings.putConfiguration(configuration);
+
+ assertThat(mThreadPersistentSettings.getConfiguration()).isEqualTo(configuration);
+ mThreadPersistentSettings.initialize();
+ assertThat(mThreadPersistentSettings.getConfiguration()).isEqualTo(configuration);
+ }
+
+ @Test
+ public void putConfiguration_dhcp6PdEnabled_valuesUpdatedAndPersisted() throws Exception {
+ ThreadConfiguration configuration =
+ new ThreadConfiguration.Builder().setDhcp6PdEnabled(true).build();
+ mThreadPersistentSettings.putConfiguration(configuration);
+
+ assertThat(mThreadPersistentSettings.getConfiguration()).isEqualTo(configuration);
+ mThreadPersistentSettings.initialize();
+ assertThat(mThreadPersistentSettings.getConfiguration()).isEqualTo(configuration);
+ }
+
+ private AtomicFile createAtomicFile() throws Exception {
+ return new AtomicFile(mTemporaryFolder.newFile());
}
private byte[] createXmlForParsing(String key, Boolean value) throws Exception {
@@ -164,19 +210,9 @@
return outputStream.toByteArray();
}
- private void setupAtomicFileMockForRead(byte[] dataToRead) throws Exception {
- FileInputStream is = mock(FileInputStream.class);
- when(mAtomicFile.openRead()).thenReturn(is);
- when(is.available()).thenReturn(dataToRead.length).thenReturn(0);
- doAnswer(
- invocation -> {
- byte[] data = invocation.getArgument(0);
- int pos = invocation.getArgument(1);
- if (pos == dataToRead.length) return 0; // read complete.
- System.arraycopy(dataToRead, 0, data, 0, dataToRead.length);
- return dataToRead.length;
- })
- .when(is)
- .read(any(), anyInt(), anyInt());
+ private void setupAtomicFileForRead(byte[] dataToRead) throws Exception {
+ try (FileOutputStream outputStream = new FileOutputStream(mAtomicFile.getBaseFile())) {
+ outputStream.write(dataToRead);
+ }
}
}