From 7520abf333974b794733cfbac8696670855baf1b Mon Sep 17 00:00:00 2001 From: Handa Wang Date: Thu, 20 Jun 2024 09:43:45 +0000 Subject: [PATCH 1/2] [Thread] add support for configuration at ThreadNetworkController This CL adds methods for setting the ThreadConfiguration and subscribing/unsubscribing to Configuration changes. The ThreadConfiguration is used for controlling Thread features for the device. Bug: 342519412 Change-Id: I9e20391eb3b2f20312a2cb0b587cc5bdf200d24d --- .../net/thread/IConfigurationReceiver.aidl | 24 +++ .../net/thread/IThreadNetworkController.aidl | 7 +- .../net/thread/ThreadConfiguration.aidl | 19 ++ .../net/thread/ThreadConfiguration.java | 170 ++++++++++++++++++ .../net/thread/ThreadNetworkController.java | 120 +++++++++++++ .../net/thread/ThreadNetworkFlags.java | 4 + 6 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 thread/framework/java/android/net/thread/IConfigurationReceiver.aidl create mode 100644 thread/framework/java/android/net/thread/ThreadConfiguration.aidl create mode 100644 thread/framework/java/android/net/thread/ThreadConfiguration.java 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 0000000000..dcc4545515 --- /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 c5ca55745d..f50de74561 100644 --- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl +++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl @@ -19,11 +19,13 @@ package android.net.thread; 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 @@ interface IThreadNetworkController { 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 0000000000..94734112b9 --- /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 0000000000..e09b3a6d88 --- /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. + * + *

An example usage of creating a {@link ThreadConfiguration} that turns on NAT64 feature based + * on an existing {@link ThreadConfiguration}: + * + *

{@code
+ * ThreadConfiguration config =
+ *     new ThreadConfiguration.Builder(existingConfig).setNat64Enabled(true).build();
+ * }
+ * + * @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 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. + * + *

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

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 8d6b40a5ce..30b3d6a6a0 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.time.Duration; 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 @@ public final class ThreadNetworkController { private final Map mOpDatasetCallbackMap = new HashMap<>(); + private final Object mConfigurationCallbackMapLock = new Object(); + + @GuardedBy("mConfigurationCallbackMapLock") + private final Map, ConfigurationCallbackProxy> + mConfigurationCallbackMap = new HashMap<>(); + /** @hide */ public ThreadNetworkController(@NonNull IThreadNetworkController controllerService) { requireNonNull(controllerService, "controllerService cannot be null"); @@ -578,6 +585,97 @@ public final class ThreadNetworkController { } } + /** + * Configures the Thread features for this device. + * + *

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

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 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 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. * @@ -764,4 +862,26 @@ public final class ThreadNetworkController { propagateError(mExecutor, mResultReceiver, errorCode, errorMessage); } } + + private static final class ConfigurationCallbackProxy extends IConfigurationReceiver.Stub { + final Executor mExecutor; + final Consumer mConfigurationConsumer; + + ConfigurationCallbackProxy( + @CallbackExecutor Executor executor, + Consumer 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 e6ab9881b8..691bbf5877 100644 --- a/thread/framework/java/android/net/thread/ThreadNetworkFlags.java +++ b/thread/framework/java/android/net/thread/ThreadNetworkFlags.java @@ -27,5 +27,9 @@ public final class ThreadNetworkFlags { /** @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() {} } From 46889108107b41d56c10d53a1996c2356b6ad534 Mon Sep 17 00:00:00 2001 From: Handa Wang Date: Wed, 12 Jun 2024 05:30:09 +0000 Subject: [PATCH 2/2] [Thread] support ThreadConfiguration API at ThreadNetworkControllerService Bug: 342519412 Test: atest CtsThreadNetworkTestCases Change-Id: I7b84a64d24e513bd6ba07bb26e3801b72c250810 --- .../ThreadNetworkControllerService.java | 75 ++++++++++++ .../thread/ThreadPersistentSettings.java | 36 ++++++ .../ThreadNetworkControllerServiceTest.java | 32 +++++ .../thread/ThreadPersistentSettingsTest.java | 114 ++++++++++++------ 4 files changed, 218 insertions(+), 39 deletions(-) diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java index 0c77deef08..a796486ca3 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; 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 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub private final OtDaemonCallbackProxy mOtDaemonCallbackProxy = new OtDaemonCallbackProxy(); private final ConnectivityResources mResources; private final Supplier mCountryCodeSupplier; + private final Map 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,6 +522,77 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub } } + @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() { diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java index f18aac924f..747cc96ecd 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 @@ package com.android.server.thread; 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 @@ public class ThreadPersistentSettings { /** Stores the Thread country code, null if no country code is stored. */ public static final Key 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 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 CONFIG_DHCP6_PD_ENABLED = + new Key<>("config_dhcp6_pd_enabled", false); + /******** Thread persistent setting keys ***************/ @GuardedBy("mLock") @@ -174,6 +186,30 @@ public class ThreadPersistentSettings { return getObject(key.key, key.defaultValue); } + /** + * 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. * 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 2f58943907..9d0cb61d7d 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.NetworkProvider; 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; @@ -708,4 +709,35 @@ public final class ThreadNetworkControllerServiceTest { 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 7d2fe91d69..c932ac8f0e 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.android.server.thread.ThreadPersistentSettings.THREAD_ENABLED; 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 com.android.server.connectivity.ConnectivityResources; 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 @@ import java.io.FileOutputStream; 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 @@ public class ThreadPersistentSettingsTest { 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 @@ public class ThreadPersistentSettingsTest { @Test public void initialize_readsFromFile() throws Exception { byte[] data = createXmlForParsing(THREAD_ENABLED.key, false); - setupAtomicFileMockForRead(data); + setupAtomicFileForRead(data); mThreadPersistentSettings.initialize(); @@ -95,7 +94,7 @@ public class ThreadPersistentSettingsTest { @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 @@ public class ThreadPersistentSettingsTest { 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 @@ public class ThreadPersistentSettingsTest { 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 @@ public class ThreadPersistentSettingsTest { 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 @@ public class ThreadPersistentSettingsTest { 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 @@ public class ThreadPersistentSettingsTest { 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 @@ public class ThreadPersistentSettingsTest { 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); + } } }