Merge "vmconfig: Introduce get_debug_level()" into main
diff --git a/compos/apex/Android.bp b/compos/apex/Android.bp
index 55cc446..9996e4e 100644
--- a/compos/apex/Android.bp
+++ b/compos/apex/Android.bp
@@ -51,6 +51,13 @@
"compsvc",
],
+ native_shared_libs: [
+ // b/334192594: compsvc has a transitive dependency to libminijail.
+ // Adding it explicitly here is required because the existence of
+ // it in Microdroid cannot be guaranteed.
+ "libminijail",
+ ],
+
systemserverclasspath_fragments: ["com.android.compos-systemserverclasspath-fragment"],
apps: [
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachine.java b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
index 51b91ca..1076219 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachine.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
@@ -63,6 +63,7 @@
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
+import android.view.KeyEvent;
import android.view.MotionEvent;
import android.system.virtualizationcommon.DeathReason;
import android.system.virtualizationcommon.ErrorCode;
@@ -102,6 +103,7 @@
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -161,6 +163,7 @@
public static final long MAX_VSOCK_PORT = (1L << 32) - 1;
private ParcelFileDescriptor mTouchSock;
+ private ParcelFileDescriptor mKeySock;
/**
* Status of a virtual machine
@@ -861,25 +864,48 @@
// Handle input devices here
List<InputDevice> inputDevices = new ArrayList<>();
if (vmConfig.getCustomImageConfig() != null
- && vmConfig.getCustomImageConfig().useTouch()
&& rawConfig.displayConfig != null) {
- ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createSocketPair();
- mTouchSock = pfds[0];
- InputDevice.SingleTouch t = new InputDevice.SingleTouch();
- t.width = rawConfig.displayConfig.width;
- t.height = rawConfig.displayConfig.height;
- t.pfd = pfds[1];
- inputDevices.add(InputDevice.singleTouch(t));
+ if (vmConfig.getCustomImageConfig().useTouch()) {
+ ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createSocketPair();
+ mTouchSock = pfds[0];
+ InputDevice.SingleTouch t = new InputDevice.SingleTouch();
+ t.width = rawConfig.displayConfig.width;
+ t.height = rawConfig.displayConfig.height;
+ t.pfd = pfds[1];
+ inputDevices.add(InputDevice.singleTouch(t));
+ }
+ if (vmConfig.getCustomImageConfig().useKeyboard()) {
+ ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createSocketPair();
+ mKeySock = pfds[0];
+ InputDevice.Keyboard k = new InputDevice.Keyboard();
+ k.pfd = pfds[1];
+ inputDevices.add(InputDevice.keyboard(k));
+ }
}
rawConfig.inputDevices = inputDevices.toArray(new InputDevice[0]);
return android.system.virtualizationservice.VirtualMachineConfig.rawConfig(rawConfig);
}
- private void addInputEvent(ByteBuffer buffer, short type, short code, int value) {
- buffer.putShort(type);
- buffer.putShort(code);
- buffer.putInt(value);
+ private static record InputEvent(short type, short code, int value) {}
+
+ /** @hide */
+ public boolean sendKeyEvent(KeyEvent event) {
+ if (mKeySock == null) {
+ Log.d(TAG, "mKeySock == null");
+ return false;
+ }
+ // from include/uapi/linux/input-event-codes.h in the kernel.
+ short EV_SYN = 0x00;
+ short EV_KEY = 0x01;
+ short SYN_REPORT = 0x00;
+ boolean down = event.getAction() != MotionEvent.ACTION_UP;
+
+ return writeEventsToSock(
+ mKeySock,
+ Arrays.asList(
+ new InputEvent(EV_KEY, (short) event.getScanCode(), down ? 1 : 0),
+ new InputEvent(EV_SYN, SYN_REPORT, 0)));
}
/** @hide */
@@ -901,24 +927,30 @@
int y = (int) event.getY();
boolean down = event.getAction() != MotionEvent.ACTION_UP;
+ return writeEventsToSock(
+ mTouchSock,
+ Arrays.asList(
+ new InputEvent(EV_ABS, ABS_X, x),
+ new InputEvent(EV_ABS, ABS_Y, y),
+ new InputEvent(EV_KEY, BTN_TOUCH, down ? 1 : 0),
+ new InputEvent(EV_SYN, SYN_REPORT, 0)));
+ }
+
+ private boolean writeEventsToSock(ParcelFileDescriptor sock, List<InputEvent> evtList) {
ByteBuffer byteBuffer =
- ByteBuffer.allocate(32 /* (type: u16 + code: u16 + value: i32) * 4 */);
+ ByteBuffer.allocate(8 /* (type: u16 + code: u16 + value: i32) */ * evtList.size());
byteBuffer.clear();
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
-
- addInputEvent(byteBuffer, EV_ABS, ABS_X, x);
- addInputEvent(byteBuffer, EV_ABS, ABS_Y, y);
- addInputEvent(byteBuffer, EV_KEY, BTN_TOUCH, down ? 1 : 0);
- addInputEvent(byteBuffer, EV_SYN, SYN_REPORT, 0);
-
+ for (InputEvent e : evtList) {
+ byteBuffer.putShort(e.type);
+ byteBuffer.putShort(e.code);
+ byteBuffer.putInt(e.value);
+ }
try {
IoBridge.write(
- mTouchSock.getFileDescriptor(),
- byteBuffer.array(),
- 0,
- byteBuffer.array().length);
+ sock.getFileDescriptor(), byteBuffer.array(), 0, byteBuffer.array().length);
} catch (IOException e) {
- Log.d(TAG, "cannot send touch evt", e);
+ Log.d(TAG, "cannot send event", e);
return false;
}
return true;
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
index 7cf5893..8d294fd 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
@@ -35,6 +35,7 @@
private static final String KEY_DISK_IMAGES = "disk_images";
private static final String KEY_DISPLAY_CONFIG = "display_config";
private static final String KEY_TOUCH = "touch";
+ private static final String KEY_KEYBOARD = "keyboard";
@Nullable private final String name;
@NonNull private final String kernelPath;
@@ -44,6 +45,7 @@
@Nullable private final Disk[] disks;
@Nullable private final DisplayConfig displayConfig;
private final boolean touch;
+ private final boolean keyboard;
@Nullable
public Disk[] getDisks() {
@@ -79,6 +81,10 @@
return touch;
}
+ public boolean useKeyboard() {
+ return keyboard;
+ }
+
/** @hide */
public VirtualMachineCustomImageConfig(
String name,
@@ -88,7 +94,8 @@
String[] params,
Disk[] disks,
DisplayConfig displayConfig,
- boolean touch) {
+ boolean touch,
+ boolean keyboard) {
this.name = name;
this.kernelPath = kernelPath;
this.initrdPath = initrdPath;
@@ -97,6 +104,7 @@
this.disks = disks;
this.displayConfig = displayConfig;
this.touch = touch;
+ this.keyboard = keyboard;
}
static VirtualMachineCustomImageConfig from(PersistableBundle customImageConfigBundle) {
@@ -125,6 +133,7 @@
customImageConfigBundle.getPersistableBundle(KEY_DISPLAY_CONFIG);
builder.setDisplayConfig(DisplayConfig.from(displayConfigPb));
builder.useTouch(customImageConfigBundle.getBoolean(KEY_TOUCH));
+ builder.useKeyboard(customImageConfigBundle.getBoolean(KEY_KEYBOARD));
return builder.build();
}
@@ -154,6 +163,7 @@
.map(dc -> dc.toPersistableBundle())
.orElse(null));
pb.putBoolean(KEY_TOUCH, touch);
+ pb.putBoolean(KEY_KEYBOARD, keyboard);
return pb;
}
@@ -203,6 +213,7 @@
private List<Disk> disks = new ArrayList<>();
private DisplayConfig displayConfig;
private boolean touch;
+ private boolean keyboard;
/** @hide */
public Builder() {}
@@ -256,6 +267,12 @@
}
/** @hide */
+ public Builder useKeyboard(boolean keyboard) {
+ this.keyboard = keyboard;
+ return this;
+ }
+
+ /** @hide */
public VirtualMachineCustomImageConfig build() {
return new VirtualMachineCustomImageConfig(
this.name,
@@ -265,7 +282,8 @@
this.params.toArray(new String[0]),
this.disks.toArray(new Disk[0]),
displayConfig,
- touch);
+ touch,
+ keyboard);
}
}
diff --git a/pvmfw/src/entry.rs b/pvmfw/src/entry.rs
index 253604b..72212c3 100644
--- a/pvmfw/src/entry.rs
+++ b/pvmfw/src/entry.rs
@@ -403,12 +403,6 @@
unsafe { slice::from_raw_parts_mut(range.start.0 as *mut u8, range.end - range.start) }
}
-enum AppendedConfigType {
- Valid,
- Invalid,
- NotFound,
-}
-
enum AppendedPayload<'a> {
/// Configuration data.
Config(config::Config<'a>),
@@ -418,35 +412,29 @@
impl<'a> AppendedPayload<'a> {
fn new(data: &'a mut [u8]) -> Option<Self> {
- match Self::guess_config_type(data) {
- AppendedConfigType::Valid => {
- let config = config::Config::new(data);
- Some(Self::Config(config.unwrap()))
- }
- AppendedConfigType::NotFound if cfg!(feature = "legacy") => {
+ // The borrow checker gets confused about the ownership of data (see inline comments) so we
+ // intentionally obfuscate it using a raw pointer; see a similar issue (still not addressed
+ // in v1.77) in https://users.rust-lang.org/t/78467.
+ let data_ptr = data as *mut [u8];
+
+ // Config::new() borrows data as mutable ...
+ match config::Config::new(data) {
+ // ... so this branch has a mutable reference to data, from the Ok(Config<'a>). But ...
+ Ok(valid) => Some(Self::Config(valid)),
+ // ... if Config::new(data).is_err(), the Err holds no ref to data. However ...
+ Err(config::Error::InvalidMagic) if cfg!(feature = "legacy") => {
+ // ... the borrow checker still complains about a second mutable ref without this.
+ // SAFETY: Pointer to a valid mut (not accessed elsewhere), 'a lifetime re-used.
+ let data: &'a mut _ = unsafe { &mut *data_ptr };
+
const BCC_SIZE: usize = SIZE_4KB;
warn!("Assuming the appended data at {:?} to be a raw BCC", data.as_ptr());
Some(Self::LegacyBcc(&mut data[..BCC_SIZE]))
}
- _ => None,
- }
- }
-
- fn guess_config_type(data: &mut [u8]) -> AppendedConfigType {
- // This function is necessary to prevent the borrow checker from getting confused
- // about the ownership of data in new(); see https://users.rust-lang.org/t/78467.
- let addr = data.as_ptr();
-
- match config::Config::new(data) {
- Err(config::Error::InvalidMagic) => {
- warn!("No configuration data found at {addr:?}");
- AppendedConfigType::NotFound
- }
Err(e) => {
- error!("Invalid configuration data at {addr:?}: {e}");
- AppendedConfigType::Invalid
+ error!("Invalid configuration data at {data_ptr:?}: {e}");
+ None
}
- Ok(_) => AppendedConfigType::Valid,
}
}
diff --git a/pvmfw/src/fdt.rs b/pvmfw/src/fdt.rs
index d847ca2..9dca8af 100644
--- a/pvmfw/src/fdt.rs
+++ b/pvmfw/src/fdt.rs
@@ -199,11 +199,16 @@
opp_node: FdtNode,
) -> libfdt::Result<ArrayVec<[u64; CpuInfo::MAX_OPPTABLES]>> {
let mut table = ArrayVec::new();
- for subnode in opp_node.subnodes()? {
+ let mut opp_nodes = opp_node.subnodes()?;
+ for subnode in opp_nodes.by_ref().take(table.capacity()) {
let prop = subnode.getprop_u64(cstr!("opp-hz"))?.ok_or(FdtError::NotFound)?;
table.push(prop);
}
+ if opp_nodes.next().is_some() {
+ warn!("OPP table has more than {} entries: discarding extra nodes.", table.capacity());
+ }
+
Ok(table)
}
diff --git a/service_vm/test_apk/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java b/service_vm/test_apk/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java
index 678e56f..f456cb4 100644
--- a/service_vm/test_apk/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java
+++ b/service_vm/test_apk/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java
@@ -95,6 +95,11 @@
if (mGki == null) {
// We don't need this permission to use the microdroid kernel.
revokePermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
+ } else {
+ // The permission is needed to use the GKI kernel.
+ // Granting the permission is needed as the microdroid kernel test setup
+ // can revoke the permission before the GKI kernel test.
+ grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
}
prepareTestSetup(true /* protectedVm */, mGki);
setMaxPerformanceTaskProfile();
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index 2b5c564..e7e9ded 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -174,7 +174,7 @@
boolean updateBootconfigs) {
File signVirtApex = findTestFile("sign_virt_apex");
- RunUtil runUtil = new RunUtil();
+ RunUtil runUtil = createRunUtil();
// Set the parent dir on the PATH (e.g. <workdir>/bin)
String separator = System.getProperty("path.separator");
String path = signVirtApex.getParentFile().getPath() + separator + System.getenv("PATH");
@@ -409,7 +409,7 @@
configPath);
PipedInputStream pis = new PipedInputStream();
- Process process = RunUtil.getDefault().runCmdInBackground(args, new PipedOutputStream(pis));
+ Process process = createRunUtil().runCmdInBackground(args, new PipedOutputStream(pis));
return new VmInfo(process);
}
@@ -890,7 +890,7 @@
File sepolicyAnalyzeBin = findTestFile("sepolicy-analyze");
CommandResult result =
- RunUtil.getDefault()
+ createRunUtil()
.runTimedCmd(
10000,
sepolicyAnalyzeBin.getPath(),
@@ -1034,14 +1034,14 @@
private boolean isLz4(String path) throws Exception {
File lz4tool = findTestFile("lz4");
CommandResult result =
- new RunUtil().runTimedCmd(5000, lz4tool.getAbsolutePath(), "-t", path);
+ createRunUtil().runTimedCmd(5000, lz4tool.getAbsolutePath(), "-t", path);
return result.getStatus() == CommandStatus.SUCCESS;
}
private void decompressLz4(String inputPath, String outputPath) throws Exception {
File lz4tool = findTestFile("lz4");
CommandResult result =
- new RunUtil()
+ createRunUtil()
.runTimedCmd(
5000, lz4tool.getAbsolutePath(), "-d", "-f", inputPath, outputPath);
String out = result.getStdout();
@@ -1072,7 +1072,7 @@
List<String> command =
Arrays.asList(avbtool.getAbsolutePath(), "info_image", "--image", image_path);
CommandResult result =
- new RunUtil().runTimedCmd(5000, "/bin/bash", "-c", String.join(" ", command));
+ createRunUtil().runTimedCmd(5000, "/bin/bash", "-c", String.join(" ", command));
String out = result.getStdout();
String err = result.getStderr();
assertWithMessage(
@@ -1241,4 +1241,14 @@
assertThat(androidDevice).isNotNull();
return androidDevice;
}
+
+ // The TradeFed Dockerfile sets LD_LIBRARY_PATH to a directory with an older libc++.so, which
+ // breaks binaries that are linked against a newer libc++.so. Binaries commonly use DT_RUNPATH
+ // to find an adjacent libc++.so (e.g. `$ORIGIN/../lib64`), but LD_LIBRARY_PATH overrides
+ // DT_RUNPATH, so clear LD_LIBRARY_PATH. See b/332593805 and b/333782216.
+ private static RunUtil createRunUtil() {
+ RunUtil runUtil = new RunUtil();
+ runUtil.unsetEnvVariable("LD_LIBRARY_PATH");
+ return runUtil;
+ }
}
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index 769ac4c..0c4aa7c 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -751,6 +751,9 @@
InputDevice::EvDev(evdev) => InputDeviceOption::EvDev(clone_file(
evdev.pfd.as_ref().ok_or(anyhow!("pfd should have value"))?,
)?),
+ InputDevice::Keyboard(keyboard) => InputDeviceOption::Keyboard(clone_file(
+ keyboard.pfd.as_ref().ok_or(anyhow!("pfd should have value"))?,
+ )?),
})
}
/// Given the configuration for a disk image, assembles the `DiskFile` to pass to crosvm.
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index 4be48a5..040e552 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -161,6 +161,7 @@
pub enum InputDeviceOption {
EvDev(File),
SingleTouch { file: File, width: u32, height: u32, name: Option<String> },
+ Keyboard(File),
}
type VfioDevice = Strong<dyn IBoundDevice>;
@@ -976,12 +977,24 @@
}
if cfg!(paravirtualized_devices) {
+ // TODO(b/325929096): Need to set up network from the config
+ if rustutils::system_properties::read_bool("ro.crosvm.network.setup.done", false)
+ .unwrap_or(false)
+ {
+ command.arg("--net").arg("tap-name=crosvm_tap");
+ }
+ }
+
+ if cfg!(paravirtualized_devices) {
for input_device_option in config.input_device_options.iter() {
command.arg("--input");
command.arg(match input_device_option {
InputDeviceOption::EvDev(file) => {
format!("evdev[path={}]", add_preserved_fd(&mut preserved_fds, file))
}
+ InputDeviceOption::Keyboard(file) => {
+ format!("keyboard[path={}]", add_preserved_fd(&mut preserved_fds, file))
+ }
InputDeviceOption::SingleTouch { file, width, height, name } => format!(
"single-touch[path={},width={},height={}{}]",
add_preserved_fd(&mut preserved_fds, file),
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/InputDevice.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/InputDevice.aidl
index fe12291..712d6a9 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/InputDevice.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/InputDevice.aidl
@@ -30,7 +30,11 @@
parcelable EvDev {
ParcelFileDescriptor pfd;
}
-
+ // Keyboard input
+ parcelable Keyboard {
+ ParcelFileDescriptor pfd;
+ }
SingleTouch singleTouch;
EvDev evDev;
+ Keyboard keyboard;
}
diff --git a/vmlauncher_app/AndroidManifest.xml b/vmlauncher_app/AndroidManifest.xml
index 607a895..d800ec7 100644
--- a/vmlauncher_app/AndroidManifest.xml
+++ b/vmlauncher_app/AndroidManifest.xml
@@ -4,9 +4,11 @@
<uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
<uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
+ <uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:name="android.software.virtualization_framework" android:required="true" />
<application
- android:label="VmLauncherApp">
+ android:label="VmLauncherApp"
+ android:networkSecurityConfig="@xml/network_security_config">
<activity android:name=".MainActivity"
android:enabled="false"
android:screenOrientation="landscape"
diff --git a/vmlauncher_app/README.md b/vmlauncher_app/README.md
index 9175e57..0109f37 100644
--- a/vmlauncher_app/README.md
+++ b/vmlauncher_app/README.md
@@ -1,13 +1,16 @@
# VM launcher app
-## Building & Installing
+## Building
-Add `VmLauncherApp` into `PRODUCT_PACKAGES` and then `m`
+This app is now part of the virt APEX.
-You can also explicitly grant or revoke the permission, e.g.
+## Enabling
+
+This app is disabled by default. To re-enable it, execute the following command.
+
```
-adb shell pm grant com.android.virtualization.vmlauncher android.permission.USE_CUSTOM_VIRTUAL_MACHINE
-adb shell pm grant com.android.virtualization.vmlauncher android.permission.MANAGE_VIRTUAL_MACHINE
+adb root
+adb shell pm enable com.android.virtualization.vmlauncher/.MainActivity
```
## Running
diff --git a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
index 450f4ed..4c42bb4 100644
--- a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -37,6 +37,7 @@
import android.view.Display;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
+import android.view.KeyEvent;
import android.view.WindowManager;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
@@ -127,6 +128,7 @@
customImageConfigBuilder.setDisplayConfig(displayConfigBuilder.build());
customImageConfigBuilder.useTouch(true);
+ customImageConfigBuilder.useKeyboard(true);
configBuilder.setCustomImageConfig(customImageConfigBuilder.build());
@@ -137,6 +139,22 @@
}
@Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (mVirtualMachine == null) {
+ return false;
+ }
+ return mVirtualMachine.sendKeyEvent(event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (mVirtualMachine == null) {
+ return false;
+ }
+ return mVirtualMachine.sendKeyEvent(event);
+ }
+
+ @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
diff --git a/vmlauncher_app/res/xml/network_security_config.xml b/vmlauncher_app/res/xml/network_security_config.xml
new file mode 100644
index 0000000..f27fa56
--- /dev/null
+++ b/vmlauncher_app/res/xml/network_security_config.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<network-security-config>
+ <domain-config cleartextTrafficPermitted="true">
+ <domain includeSubdomains="true">localhost</domain>
+ </domain-config>
+</network-security-config>