Merge "[Thread] Add support for Thread persistent setting" into main am: 396cc5721e
Original change: https://android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/2894700 Change-Id: I862b6927165ddf012777a1e2842ccac578d27e4d Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
commit
3aae8fa22a
3 changed files with 371 additions and 0 deletions
|
|
@ -0,0 +1,243 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +44,7 @@ android_test {
|
|||
"service-connectivity-pre-jarjar",
|
||||
"service-thread-pre-jarjar",
|
||||
"truth",
|
||||
"service-thread-pre-jarjar",
|
||||
],
|
||||
libs: [
|
||||
"android.test.base",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue