Merge changes from topic "thread-configuration-api" into main

* changes:
  [Thread] support ThreadConfiguration API at ThreadNetworkControllerService
  [Thread] add support for configuration at ThreadNetworkController
This commit is contained in:
Treehugger Robot 2024-06-24 10:03:18 +00:00 • committed by Gerrit Code Review
commit bc9c0e2b03
10 changed files with 561 additions and 40 deletions

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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;

View file

@ -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);
}
}
}

View file

@ -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<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");
@ -578,6 +585,97 @@ public final class ThreadNetworkController {
}
}
/**
* 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.
*
@ -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<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);
}
}
}
}

View file

@ -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() {}
}

View file

@ -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<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,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() {

View file

@ -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<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")
@ -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.
*

View file

@ -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;
@ -706,4 +707,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();
}
}

View file

@ -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);
}
}
}