Merge "[Thread] make Thread state customisable via resource overlay" into main

This commit is contained in:
Kangping Dong 2024-02-28 16:28:55 +00:00 • committed by Gerrit Code Review
commit 5ce2519a3e
5 changed files with 97 additions and 50 deletions

View file

@ -20,10 +20,15 @@
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- Sets to {@code true} to enable Thread on the device by default. Note this is the default
value, the actual Thread enabled state can be changed by the {@link
ThreadNetworkController#setEnabled} API.
-->
<bool name="config_thread_default_enabled">true</bool>
<!-- Whether to use location APIs in the algorithm to determine country code or not.
If disabled, will use other sources (telephony, wifi, etc) to determine device location for
Thread Network regulatory purposes.
-->
<bool name="config_thread_location_use_for_country_code_enabled">true</bool>
</resources>

View file

@ -46,6 +46,7 @@
<item type="integer" name="config_netstats_validate_import" />
<!-- Configuration values for ThreadNetworkService -->
<item type="bool" name="config_thread_default_enabled" />
<item type="bool" name="config_thread_location_use_for_country_code_enabled" />
</policy>
</overlayable>

View file

@ -18,21 +18,16 @@ package com.android.server.thread;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
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.IThreadNetworkController;
import android.net.thread.IThreadNetworkManager;
import android.os.Binder;
import android.os.ParcelFileDescriptor;
import android.util.AtomicFile;
import com.android.server.SystemService;
import java.io.File;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Collections;
@ -51,12 +46,7 @@ public class ThreadNetworkService extends IThreadNetworkManager.Stub {
/** Creates a new {@link ThreadNetworkService} object. */
public ThreadNetworkService(Context context) {
mContext = context;
mPersistentSettings =
new ThreadPersistentSettings(
new AtomicFile(
new File(
getOrCreateThreadnetworkDir(),
ThreadPersistentSettings.FILE_NAME)));
mPersistentSettings = ThreadPersistentSettings.newInstance(context);
}
/**
@ -123,19 +113,4 @@ public class ThreadNetworkService extends IThreadNetworkManager.Stub {
pw.println();
}
/** Get device protected storage dir for the tethering apex. */
private static File getOrCreateThreadnetworkDir() {
final File threadnetworkDir;
final File apexDataDir =
ApexEnvironment.getApexEnvironment(TETHERING_MODULE_NAME)
.getDeviceProtectedDataDir();
threadnetworkDir = new File(apexDataDir, "thread");
if (threadnetworkDir.exists() || threadnetworkDir.mkdirs()) {
return threadnetworkDir;
}
throw new IllegalStateException(
"Cannot write into thread network data directory: " + threadnetworkDir);
}
}

View file

@ -16,15 +16,23 @@
package com.android.server.thread;
import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
import android.annotation.Nullable;
import android.content.ApexEnvironment;
import android.content.Context;
import android.os.PersistableBundle;
import android.util.AtomicFile;
import android.util.Log;
import com.android.connectivity.resources.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.connectivity.ConnectivityResources;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
@ -39,7 +47,7 @@ import java.io.InputStream;
public class ThreadPersistentSettings {
private static final String TAG = "ThreadPersistentSettings";
/** File name used for storing settings. */
public static final String FILE_NAME = "ThreadPersistentSettings.xml";
private static final String FILE_NAME = "ThreadPersistentSettings.xml";
/** Current config store data version. This will be incremented for any additions. */
private static final int CURRENT_SETTINGS_STORE_DATA_VERSION = 1;
/**
@ -62,16 +70,29 @@ public class ThreadPersistentSettings {
@GuardedBy("mLock")
private final PersistableBundle mSettings = new PersistableBundle();
public ThreadPersistentSettings(AtomicFile atomicFile) {
private final ConnectivityResources mResources;
public static ThreadPersistentSettings newInstance(Context context) {
return new ThreadPersistentSettings(
new AtomicFile(new File(getOrCreateThreadNetworkDir(), FILE_NAME)),
new ConnectivityResources(context));
}
@VisibleForTesting
ThreadPersistentSettings(AtomicFile atomicFile, ConnectivityResources resources) {
mAtomicFile = atomicFile;
mResources = resources;
}
/** Initialize the settings by reading from the settings file. */
public void initialize() {
readFromStoreFile();
synchronized (mLock) {
if (mSettings.isEmpty()) {
put(THREAD_ENABLED.key, THREAD_ENABLED.defaultValue);
if (!mSettings.containsKey(THREAD_ENABLED.key)) {
Log.i(TAG, "\"thread_enabled\" is missing in settings file, using default value");
put(
THREAD_ENABLED.key,
mResources.get().getBoolean(R.bool.config_thread_default_enabled));
}
}
}
@ -240,4 +261,19 @@ public class ThreadPersistentSettings {
throw e;
}
}
/** Get device protected storage dir for the tethering apex. */
private static File getOrCreateThreadNetworkDir() {
final File threadnetworkDir;
final File apexDataDir =
ApexEnvironment.getApexEnvironment(TETHERING_MODULE_NAME)
.getDeviceProtectedDataDir();
threadnetworkDir = new File(apexDataDir, "thread");
if (threadnetworkDir.exists() || threadnetworkDir.mkdirs()) {
return threadnetworkDir;
}
throw new IllegalStateException(
"Cannot write into thread network data directory: " + threadnetworkDir);
}
}

View file

@ -0,0 +1,157 @@
/*
* Copyright (C) 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 com.android.server.thread;
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.os.PersistableBundle;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.AtomicFile;
import androidx.test.runner.AndroidJUnit4;
import com.android.connectivity.resources.R;
import com.android.server.connectivity.ConnectivityResources;
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.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
/** Unit tests for {@link ThreadPersistentSettings}. */
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ThreadPersistentSettingsTest {
@Mock private AtomicFile mAtomicFile;
@Mock Resources mResources;
@Mock ConnectivityResources mConnectivityResources;
private ThreadPersistentSettings mThreadPersistentSettings;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
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);
mThreadPersistentSettings =
new ThreadPersistentSettings(mAtomicFile, mConnectivityResources);
}
/** Called after each test */
@After
public void tearDown() {
validateMockitoUsage();
}
@Test
public void initialize_readsFromFile() throws Exception {
byte[] data = createXmlForParsing(THREAD_ENABLED.key, false);
setupAtomicFileMockForRead(data);
mThreadPersistentSettings.initialize();
assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
}
@Test
public void initialize_ThreadDisabledInResources_returnsThreadDisabled() throws Exception {
when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(false);
setupAtomicFileMockForRead(new byte[0]);
mThreadPersistentSettings.initialize();
assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
}
@Test
public void initialize_ThreadDisabledInResourcesButEnabledInXml_returnsThreadEnabled()
throws Exception {
when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(false);
byte[] data = createXmlForParsing(THREAD_ENABLED.key, true);
setupAtomicFileMockForRead(data);
mThreadPersistentSettings.initialize();
assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isTrue();
}
@Test
public void put_ThreadFeatureEnabledTrue_returnsTrue() throws Exception {
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
public void put_ThreadFeatureEnabledFalse_returnsFalse() throws Exception {
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());
}
private byte[] createXmlForParsing(String key, Boolean value) throws Exception {
PersistableBundle bundle = new PersistableBundle();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
bundle.putBoolean(key, value);
bundle.writeToStream(outputStream);
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());
}
}