Set Thread Network country code from location country code
This CL selects the Thread country code from location country code.
If the location country code is not avaliable, the country code `WW`
will be selected as the default Thread country code.
This CL also adds Shell commands for developers to override the Thread
country code for testing.
Bug: b/309357909
Test: Run `atest ThreadNetworkUnitTests`.
Change-Id: Id87c293005f0e75922a72854b40c41837b74397f
diff --git a/thread/tests/unit/Android.bp b/thread/tests/unit/Android.bp
index 8092693..74b4a35 100644
--- a/thread/tests/unit/Android.bp
+++ b/thread/tests/unit/Android.bp
@@ -31,14 +31,15 @@
"general-tests",
],
static_libs: [
- "androidx.test.ext.junit",
- "compatibility-device-util-axt",
+ "frameworks-base-testutils",
"framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
+ "framework-location.stubs.module_lib",
"guava",
"guava-android-testlib",
- "mockito-target-minus-junit4",
+ "mockito-target-extended-minus-junit4",
"net-tests-utils",
+ "service-thread-pre-jarjar",
"truth",
],
libs: [
@@ -46,6 +47,11 @@
"android.test.runner",
],
jarjar_rules: ":connectivity-jarjar-rules",
+ jni_libs: [
+ // these are needed for Extended Mockito
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ ],
// Test coverage system runs on different devices. Need to
// compile for all architectures.
compile_multilib: "both",
diff --git a/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
index 7284968..e92dcb9 100644
--- a/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
+++ b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
@@ -33,12 +33,8 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import java.security.SecureRandom;
-import java.util.Random;
-
/** Unit tests for {@link ActiveOperationalDataset}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -62,9 +58,6 @@
+ "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ "B9D351B40C0402A0FFF8");
- @Mock private Random mockRandom;
- @Mock private SecureRandom mockSecureRandom;
-
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
diff --git a/thread/tests/unit/src/com/android/server/thread/BinderUtil.java b/thread/tests/unit/src/com/android/server/thread/BinderUtil.java
new file mode 100644
index 0000000..3614bce
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/BinderUtil.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import android.os.Binder;
+
+/** Utilities for faking the calling uid in Binder. */
+public class BinderUtil {
+ /**
+ * Fake the calling uid in Binder.
+ *
+ * @param uid the calling uid that Binder should return from now on
+ */
+ public static void setUid(int uid) {
+ Binder.restoreCallingIdentity((((long) uid) << 32) | Binder.getCallingPid());
+ }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
new file mode 100644
index 0000000..f51aa0a
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
+
+import static com.android.server.thread.ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyDouble;
+import static org.mockito.Mockito.anyFloat;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.pm.PackageManager;
+import android.location.Address;
+import android.location.Geocoder;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.net.thread.IOperationReceiver;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
+
+import java.util.List;
+import java.util.Locale;
+
+/** Unit tests for {@link ThreadNetworkCountryCode}. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ThreadNetworkCountryCodeTest {
+ private static final String TEST_COUNTRY_CODE_US = "US";
+ private static final String TEST_COUNTRY_CODE_CN = "CN";
+
+ @Mock LocationManager mLocationManager;
+ @Mock Geocoder mGeocoder;
+ @Mock ThreadNetworkControllerService mThreadNetworkControllerService;
+ @Mock PackageManager mPackageManager;
+ @Mock Location mLocation;
+
+ private ThreadNetworkCountryCode mThreadNetworkCountryCode;
+ private boolean mErrorSetCountryCode;
+
+ @Captor private ArgumentCaptor<LocationListener> mLocationListenerCaptor;
+ @Captor private ArgumentCaptor<Geocoder.GeocodeListener> mGeocodeListenerCaptor;
+ @Captor private ArgumentCaptor<IOperationReceiver> mOperationReceiverCaptor;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ when(mLocation.getLatitude()).thenReturn(0.0);
+ when(mLocation.getLongitude()).thenReturn(0.0);
+
+ Answer setCountryCodeCallback =
+ invocation -> {
+ Object[] args = invocation.getArguments();
+ IOperationReceiver cb = (IOperationReceiver) args[1];
+
+ if (mErrorSetCountryCode) {
+ cb.onError(ERROR_INTERNAL_ERROR, new String("Invalid country code"));
+ } else {
+ cb.onSuccess();
+ }
+ return new Object();
+ };
+
+ doAnswer(setCountryCodeCallback)
+ .when(mThreadNetworkControllerService)
+ .setCountryCode(any(), any(IOperationReceiver.class));
+
+ mThreadNetworkCountryCode =
+ new ThreadNetworkCountryCode(
+ mLocationManager, mThreadNetworkControllerService, mGeocoder);
+ }
+
+ private static Address newAddress(String countryCode) {
+ Address address = new Address(Locale.ROOT);
+ address.setCountryCode(countryCode);
+ return address;
+ }
+
+ @Test
+ public void initialize_defaultCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(DEFAULT_COUNTRY_CODE);
+ }
+
+ @Test
+ public void locationCountryCode_locationChanged_locationCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+
+ verify(mLocationManager)
+ .requestLocationUpdates(
+ anyString(), anyLong(), anyFloat(), mLocationListenerCaptor.capture());
+ mLocationListenerCaptor.getValue().onLocationChanged(mLocation);
+ verify(mGeocoder)
+ .getFromLocation(
+ anyDouble(), anyDouble(), anyInt(), mGeocodeListenerCaptor.capture());
+ mGeocodeListenerCaptor.getValue().onGeocode(List.of(newAddress(TEST_COUNTRY_CODE_US)));
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_US);
+ }
+
+ @Test
+ public void updateCountryCode_noForceUpdateDefaultCountryCode_noCountryCodeIsUpdated() {
+ mThreadNetworkCountryCode.initialize();
+ clearInvocations(mThreadNetworkControllerService);
+
+ mThreadNetworkCountryCode.updateCountryCode(false /* forceUpdate */);
+
+ verify(mThreadNetworkControllerService, never()).setCountryCode(any(), any());
+ }
+
+ @Test
+ public void updateCountryCode_forceUpdateDefaultCountryCode_countryCodeIsUpdated() {
+ mThreadNetworkCountryCode.initialize();
+ clearInvocations(mThreadNetworkControllerService);
+
+ mThreadNetworkCountryCode.updateCountryCode(true /* forceUpdate */);
+
+ verify(mThreadNetworkControllerService)
+ .setCountryCode(eq(DEFAULT_COUNTRY_CODE), mOperationReceiverCaptor.capture());
+ }
+
+ @Test
+ public void setOverrideCountryCode_defaultCountryCodeAvailable_overrideCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+
+ mThreadNetworkCountryCode.setOverrideCountryCode(TEST_COUNTRY_CODE_CN);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+ }
+
+ @Test
+ public void clearOverrideCountryCode_defaultCountryCodeAvailable_defaultCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+ mThreadNetworkCountryCode.setOverrideCountryCode(TEST_COUNTRY_CODE_CN);
+
+ mThreadNetworkCountryCode.clearOverrideCountryCode();
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(DEFAULT_COUNTRY_CODE);
+ }
+
+ @Test
+ public void setCountryCodeFailed_defaultCountryCodeAvailable_countryCodeIsNotUpdated() {
+ mThreadNetworkCountryCode.initialize();
+
+ mErrorSetCountryCode = true;
+ mThreadNetworkCountryCode.setOverrideCountryCode(TEST_COUNTRY_CODE_CN);
+
+ verify(mThreadNetworkControllerService)
+ .setCountryCode(eq(TEST_COUNTRY_CODE_CN), mOperationReceiverCaptor.capture());
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(DEFAULT_COUNTRY_CODE);
+ }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
new file mode 100644
index 0000000..c7e0eca
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.contains;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.validateMockitoUsage;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.Binder;
+import android.os.Process;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/** Unit tests for {@link ThreadNetworkShellCommand}. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ThreadNetworkShellCommandTest {
+ private static final String TAG = "ThreadNetworkShellCommandTTest";
+ @Mock ThreadNetworkService mThreadNetworkService;
+ @Mock ThreadNetworkCountryCode mThreadNetworkCountryCode;
+ @Mock PrintWriter mErrorWriter;
+ @Mock PrintWriter mOutputWriter;
+
+ ThreadNetworkShellCommand mThreadNetworkShellCommand;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mThreadNetworkShellCommand = new ThreadNetworkShellCommand(mThreadNetworkCountryCode);
+ mThreadNetworkShellCommand.setPrintWriters(mOutputWriter, mErrorWriter);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ validateMockitoUsage();
+ }
+
+ @Test
+ public void getCountryCode_executeInUnrootedShell_allowed() {
+ BinderUtil.setUid(Process.SHELL_UID);
+ when(mThreadNetworkCountryCode.getCountryCode()).thenReturn("US");
+
+ mThreadNetworkShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"get-country-code"});
+
+ verify(mOutputWriter).println(contains("US"));
+ }
+
+ @Test
+ public void forceSetCountryCodeEnabled_executeInUnrootedShell_notAllowed() {
+ BinderUtil.setUid(Process.SHELL_UID);
+
+ mThreadNetworkShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"force-country-code", "enabled", "US"});
+
+ verify(mThreadNetworkCountryCode, never()).setOverrideCountryCode(eq("US"));
+ verify(mErrorWriter).println(contains("force-country-code"));
+ }
+
+ @Test
+ public void forceSetCountryCodeEnabled_executeInRootedShell_allowed() {
+ BinderUtil.setUid(Process.ROOT_UID);
+
+ mThreadNetworkShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"force-country-code", "enabled", "US"});
+
+ verify(mThreadNetworkCountryCode).setOverrideCountryCode(eq("US"));
+ }
+
+ @Test
+ public void forceSetCountryCodeDisabled_executeInUnrootedShell_notAllowed() {
+ BinderUtil.setUid(Process.SHELL_UID);
+
+ mThreadNetworkShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"force-country-code", "disabled"});
+
+ verify(mThreadNetworkCountryCode, never()).setOverrideCountryCode(any());
+ verify(mErrorWriter).println(contains("force-country-code"));
+ }
+
+ @Test
+ public void forceSetCountryCodeDisabled_executeInRootedShell_allowed() {
+ BinderUtil.setUid(Process.ROOT_UID);
+
+ mThreadNetworkShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"force-country-code", "disabled"});
+
+ verify(mThreadNetworkCountryCode).clearOverrideCountryCode();
+ }
+}