Skip to content
Snippets Groups Projects
Commit 95f40d0c authored by Yang Sun's avatar Yang Sun
Browse files

[Thread] Add support for Thread persistent setting

The ThreadPersistentSetting class can be used to read/store key value
pairs in ThreadPersistentSetting.xml file.

Bug: 299243765

Test: atest ThreadNetworkUnitTests:com.android.server.thread.ThreadPersistentSettingTest
Change-Id: I564ce8373e6af8f5cdf066a579bec46bfffecb72
parent 1bf52733
No related branches found
No related tags found
No related merge requests found
/*
* 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 android.annotation.Nullable;
import android.os.PersistableBundle;
import android.util.AtomicFile;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Store persistent data for Thread network settings. These are key (string) / value pairs that are
* stored in ThreadPersistentSetting.xml file. The values allowed are those that can be serialized
* via {@link PersistableBundle}.
*/
public class ThreadPersistentSettings {
private static final String TAG = "ThreadPersistentSettings";
/** File name used for storing settings. */
public 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;
/**
* Stores the version of the data. This can be used to handle migration of data if some
* non-backward compatible change introduced.
*/
private static final String VERSION_KEY = "version";
/******** Thread persistent setting keys ***************/
/** Stores the Thread feature toggle state, true for enabled and false for disabled. */
public static final Key<Boolean> THREAD_ENABLED = new Key<>("Thread_enabled", true);
/******** Thread persistent setting keys ***************/
@GuardedBy("mLock")
private final AtomicFile mAtomicFile;
private final Object mLock = new Object();
@GuardedBy("mLock")
private final PersistableBundle mSettings = new PersistableBundle();
public ThreadPersistentSettings(AtomicFile atomicFile) {
mAtomicFile = atomicFile;
}
/** 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);
}
}
}
private void putObject(String key, @Nullable Object value) {
synchronized (mLock) {
if (value == null) {
mSettings.putString(key, null);
} else if (value instanceof Boolean) {
mSettings.putBoolean(key, (Boolean) value);
} else if (value instanceof Integer) {
mSettings.putInt(key, (Integer) value);
} else if (value instanceof Long) {
mSettings.putLong(key, (Long) value);
} else if (value instanceof Double) {
mSettings.putDouble(key, (Double) value);
} else if (value instanceof String) {
mSettings.putString(key, (String) value);
} else {
throw new IllegalArgumentException("Unsupported type " + value.getClass());
}
}
}
private <T> T getObject(String key, T defaultValue) {
Object value;
synchronized (mLock) {
if (defaultValue instanceof Boolean) {
value = mSettings.getBoolean(key, (Boolean) defaultValue);
} else if (defaultValue instanceof Integer) {
value = mSettings.getInt(key, (Integer) defaultValue);
} else if (defaultValue instanceof Long) {
value = mSettings.getLong(key, (Long) defaultValue);
} else if (defaultValue instanceof Double) {
value = mSettings.getDouble(key, (Double) defaultValue);
} else if (defaultValue instanceof String) {
value = mSettings.getString(key, (String) defaultValue);
} else {
throw new IllegalArgumentException("Unsupported type " + defaultValue.getClass());
}
}
return (T) value;
}
/**
* Store a value to the stored settings.
*
* @param key One of the settings keys.
* @param value Value to be stored.
*/
public <T> void put(String key, @Nullable T value) {
putObject(key, value);
writeToStoreFile();
}
/**
* Retrieve a value from the stored settings.
*
* @param key One of the settings keys.
* @return value stored in settings, defValue if the key does not exist.
*/
public <T> T get(Key<T> key) {
return getObject(key.key, key.defaultValue);
}
/**
* Base class to store string key and its default value.
*
* @param <T> Type of the value.
*/
public static class Key<T> {
public final String key;
public final T defaultValue;
private Key(String key, T defaultValue) {
this.key = key;
this.defaultValue = defaultValue;
}
@Override
public String toString() {
return "[Key: " + key + ", DefaultValue: " + defaultValue + "]";
}
}
private void writeToStoreFile() {
try {
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
final PersistableBundle bundleToWrite;
synchronized (mLock) {
bundleToWrite = new PersistableBundle(mSettings);
}
bundleToWrite.putInt(VERSION_KEY, CURRENT_SETTINGS_STORE_DATA_VERSION);
bundleToWrite.writeToStream(outputStream);
synchronized (mLock) {
writeToAtomicFile(mAtomicFile, outputStream.toByteArray());
}
} catch (IOException e) {
Log.wtf(TAG, "Write to store file failed", e);
}
}
private void readFromStoreFile() {
try {
final byte[] readData;
synchronized (mLock) {
Log.i(TAG, "Reading from store file: " + mAtomicFile.getBaseFile());
readData = readFromAtomicFile(mAtomicFile);
}
final ByteArrayInputStream inputStream = new ByteArrayInputStream(readData);
final PersistableBundle bundleRead = PersistableBundle.readFromStream(inputStream);
// Version unused for now. May be needed in the future for handling migrations.
bundleRead.remove(VERSION_KEY);
synchronized (mLock) {
mSettings.putAll(bundleRead);
}
} catch (FileNotFoundException e) {
Log.e(TAG, "No store file to read", e);
} catch (IOException e) {
Log.e(TAG, "Read from store file failed", e);
}
}
/**
* Read raw data from the atomic file. Note: This is a copy of {@link AtomicFile#readFully()}
* modified to use the passed in {@link InputStream} which was returned using {@link
* AtomicFile#openRead()}.
*/
private static byte[] readFromAtomicFile(AtomicFile file) throws IOException {
FileInputStream stream = null;
try {
stream = file.openRead();
int pos = 0;
int avail = stream.available();
byte[] data = new byte[avail];
while (true) {
int amt = stream.read(data, pos, data.length - pos);
if (amt <= 0) {
return data;
}
pos += amt;
avail = stream.available();
if (avail > data.length - pos) {
byte[] newData = new byte[pos + avail];
System.arraycopy(data, 0, newData, 0, pos);
data = newData;
}
}
} finally {
if (stream != null) stream.close();
}
}
/** Write the raw data to the atomic file. */
private static void writeToAtomicFile(AtomicFile file, byte[] data) throws IOException {
// Write the data to the atomic file.
FileOutputStream out = null;
try {
out = file.startWrite();
out.write(data);
file.finishWrite(out);
} catch (IOException e) {
if (out != null) {
file.failWrite(out);
}
throw e;
}
}
}
......@@ -40,6 +40,7 @@ android_test {
"mockito-target-minus-junit4",
"net-tests-utils",
"truth",
"service-thread-pre-jarjar",
],
libs: [
"android.test.base",
......
/*
* 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.mock;
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.PersistableBundle;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.AtomicFile;
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.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;
private ThreadPersistentSettings mThreadPersistentSetting;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
FileOutputStream fos = mock(FileOutputStream.class);
when(mAtomicFile.startWrite()).thenReturn(fos);
mThreadPersistentSetting = new ThreadPersistentSettings(mAtomicFile);
}
/** Called after each test */
@After
public void tearDown() {
validateMockitoUsage();
}
@Test
public void put_ThreadFeatureEnabledTrue_returnsTrue() throws Exception {
mThreadPersistentSetting.put(THREAD_ENABLED.key, true);
assertThat(mThreadPersistentSetting.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 {
mThreadPersistentSetting.put(THREAD_ENABLED.key, false);
assertThat(mThreadPersistentSetting.get(THREAD_ENABLED)).isFalse();
// Confirm that file writes have been triggered.
verify(mAtomicFile).startWrite();
verify(mAtomicFile).finishWrite(any());
}
@Test
public void initialize_readsFromFile() throws Exception {
byte[] data = createXmlForParsing(THREAD_ENABLED.key, false);
setupAtomicFileMockForRead(data);
// Trigger file read.
mThreadPersistentSetting.initialize();
assertThat(mThreadPersistentSetting.get(THREAD_ENABLED)).isFalse();
verify(mAtomicFile, never()).startWrite();
}
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());
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment