[automerge] radio: provide hardcoded IMEI and null IMEI-SV 2p: 5154019c1f am: fa19a971a2

Original change: https://googleplex-android-review.googlesource.com/c/device/google/cuttlefish/+/18620247

Change-Id: Ieef130f552d315d3bce71115940dcfcc44968b01
Signed-off-by: Automerger Merge Worker <[email protected]>
diff --git a/common/libs/device_config/host_device_config.cpp b/common/libs/device_config/host_device_config.cpp
index 35ec27f..eb28c63 100644
--- a/common/libs/device_config/host_device_config.cpp
+++ b/common/libs/device_config/host_device_config.cpp
@@ -163,8 +163,9 @@
 
     device_display_config->set_width(cuttlefish_display_config.width);
     device_display_config->set_height(cuttlefish_display_config.height);
-    device_display_config->set_dpi(cuttlefish_config.dpi());
-    device_display_config->set_refresh_rate_hz(cuttlefish_config.refresh_rate_hz());
+    device_display_config->set_dpi(cuttlefish_display_config.dpi);
+    device_display_config->set_refresh_rate_hz(
+        cuttlefish_display_config.refresh_rate_hz);
   }
 }
 
diff --git a/common/libs/fs/shared_fd.cpp b/common/libs/fs/shared_fd.cpp
index 8b21f0c..b770a38 100644
--- a/common/libs/fs/shared_fd.cpp
+++ b/common/libs/fs/shared_fd.cpp
@@ -444,7 +444,7 @@
   return rval;
 }
 
-SharedFD SharedFD::VsockServer(unsigned int port, int type) {
+SharedFD SharedFD::VsockServer(unsigned int port, int type, unsigned int cid) {
   auto vsock = SharedFD::Socket(AF_VSOCK, type, 0);
   if (!vsock->IsOpen()) {
     return vsock;
@@ -452,7 +452,7 @@
   sockaddr_vm addr{};
   addr.svm_family = AF_VSOCK;
   addr.svm_port = port;
-  addr.svm_cid = VMADDR_CID_ANY;
+  addr.svm_cid = cid;
   auto casted_addr = reinterpret_cast<sockaddr*>(&addr);
   if (vsock->Bind(casted_addr, sizeof(addr)) == -1) {
     LOG(ERROR) << "Bind failed (" << vsock->StrError() << ")";
diff --git a/common/libs/fs/shared_fd.h b/common/libs/fs/shared_fd.h
index e712ab9..114d199 100644
--- a/common/libs/fs/shared_fd.h
+++ b/common/libs/fs/shared_fd.h
@@ -141,7 +141,8 @@
   static SharedFD SocketLocalServer(const std::string& name, bool is_abstract,
                                     int in_type, mode_t mode);
   static SharedFD SocketLocalServer(int port, int type);
-  static SharedFD VsockServer(unsigned int port, int type);
+  static SharedFD VsockServer(unsigned int port, int type,
+                              unsigned int cid = VMADDR_CID_ANY);
   static SharedFD VsockServer(int type);
   static SharedFD VsockClient(unsigned int cid, unsigned int port, int type);
 
diff --git a/common/libs/utils/Android.bp b/common/libs/utils/Android.bp
index f8ca5e2..860b684 100644
--- a/common/libs/utils/Android.bp
+++ b/common/libs/utils/Android.bp
@@ -30,18 +30,21 @@
         "base64.cpp",
         "tcp_socket.cpp",
         "tee_logging.cpp",
+        "vsock_connection.cpp",
     ],
     shared: {
         shared_libs: [
             "libbase",
             "libcuttlefish_fs",
             "libcrypto",
+            "libjsoncpp",
         ],
     },
     static: {
         static_libs: [
             "libbase",
             "libcuttlefish_fs",
+            "libjsoncpp",
         ],
         shared_libs: [
           "libcrypto", // libcrypto_static is not accessible from all targets
@@ -49,3 +52,12 @@
     },
     defaults: ["cuttlefish_host"],
 }
+
+cc_library {
+    name: "libvsock_utils",
+    srcs: ["vsock_connection.cpp"],
+    shared_libs: ["libbase", "libcuttlefish_fs", "liblog", "libjsoncpp"],
+    defaults: ["cuttlefish_guest_only"],
+    include_dirs: ["device/google/cuttlefish"],
+    export_include_dirs: ["."],
+}
diff --git a/common/libs/utils/vsock_connection.cpp b/common/libs/utils/vsock_connection.cpp
new file mode 100644
index 0000000..6142b69
--- /dev/null
+++ b/common/libs/utils/vsock_connection.cpp
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+#include "common/libs/utils/vsock_connection.h"
+
+#include "common/libs/fs/shared_buf.h"
+#include "common/libs/fs/shared_select.h"
+
+#include "android-base/logging.h"
+
+namespace cuttlefish {
+
+VsockConnection::~VsockConnection() { Disconnect(); }
+
+std::future<bool> VsockConnection::ConnectAsync(unsigned int port,
+                                                unsigned int cid) {
+  return std::async(std::launch::async,
+                    [this, port, cid]() { return Connect(port, cid); });
+}
+
+void VsockConnection::Disconnect() {
+  LOG(INFO) << "Disconnecting with fd status:" << fd_->StrError();
+  fd_->Shutdown(SHUT_RDWR);
+  if (disconnect_callback_) {
+    disconnect_callback_();
+  }
+  fd_->Close();
+}
+
+void VsockConnection::SetDisconnectCallback(std::function<void()> callback) {
+  disconnect_callback_ = callback;
+}
+
+bool VsockConnection::IsConnected() const { return fd_->IsOpen(); }
+
+bool VsockConnection::DataAvailable() const {
+  SharedFDSet read_set;
+  read_set.Set(fd_);
+  struct timeval timeout = {0, 0};
+  return Select(&read_set, nullptr, nullptr, &timeout) > 0;
+}
+
+int32_t VsockConnection::Read() {
+  std::lock_guard<std::recursive_mutex> lock(read_mutex_);
+  int32_t result;
+  if (ReadExactBinary(fd_, &result) != sizeof(result)) {
+    Disconnect();
+    return 0;
+  }
+  return result;
+}
+
+bool VsockConnection::Read(std::vector<char>& data) {
+  std::lock_guard<std::recursive_mutex> lock(read_mutex_);
+  return ReadExact(fd_, &data) == data.size();
+}
+
+std::vector<char> VsockConnection::Read(size_t size) {
+  if (size == 0) {
+    return {};
+  }
+  std::lock_guard<std::recursive_mutex> lock(read_mutex_);
+  std::vector<char> result(size);
+  if (ReadExact(fd_, &result) != size) {
+    Disconnect();
+    return {};
+  }
+  return result;
+}
+
+std::future<std::vector<char>> VsockConnection::ReadAsync(size_t size) {
+  return std::async(std::launch::async, [this, size]() { return Read(size); });
+}
+
+// Message format is buffer size followed by buffer data
+std::vector<char> VsockConnection::ReadMessage() {
+  std::lock_guard<std::recursive_mutex> lock(read_mutex_);
+  auto size = Read();
+  if (size < 0) {
+    Disconnect();
+    return {};
+  }
+  return Read(size);
+}
+
+bool VsockConnection::ReadMessage(std::vector<char>& data) {
+  std::lock_guard<std::recursive_mutex> lock(read_mutex_);
+  auto size = Read();
+  if (size < 0) {
+    Disconnect();
+    return false;
+  }
+  data.resize(size);
+  return Read(data);
+}
+
+std::future<std::vector<char>> VsockConnection::ReadMessageAsync() {
+  return std::async(std::launch::async, [this]() { return ReadMessage(); });
+}
+
+Json::Value VsockConnection::ReadJsonMessage() {
+  auto msg = ReadMessage();
+  Json::CharReaderBuilder builder;
+  Json::CharReader* reader = builder.newCharReader();
+  Json::Value json_msg;
+  std::string errors;
+  if (!reader->parse(msg.data(), msg.data() + msg.size(), &json_msg, &errors)) {
+    return {};
+  }
+  return json_msg;
+}
+
+std::future<Json::Value> VsockConnection::ReadJsonMessageAsync() {
+  return std::async(std::launch::async, [this]() { return ReadJsonMessage(); });
+}
+
+bool VsockConnection::Write(int32_t data) {
+  std::lock_guard<std::recursive_mutex> lock(write_mutex_);
+  if (WriteAllBinary(fd_, &data) != sizeof(data)) {
+    Disconnect();
+    return false;
+  }
+  return true;
+}
+
+bool VsockConnection::Write(const char* data, unsigned int size) {
+  std::lock_guard<std::recursive_mutex> lock(write_mutex_);
+  if (WriteAll(fd_, data, size) != size) {
+    Disconnect();
+    return false;
+  }
+  return true;
+}
+
+bool VsockConnection::Write(const std::vector<char>& data) {
+  return Write(data.data(), data.size());
+}
+
+// Message format is buffer size followed by buffer data
+bool VsockConnection::WriteMessage(const std::string& data) {
+  return Write(data.size()) && Write(data.c_str(), data.length());
+}
+
+bool VsockConnection::WriteMessage(const std::vector<char>& data) {
+  std::lock_guard<std::recursive_mutex> lock(write_mutex_);
+  return Write(data.size()) && Write(data);
+}
+
+bool VsockConnection::WriteMessage(const Json::Value& data) {
+  Json::StreamWriterBuilder factory;
+  std::string message_str = Json::writeString(factory, data);
+  return WriteMessage(message_str);
+}
+
+bool VsockConnection::WriteStrides(const char* data, unsigned int size,
+                                   unsigned int num_strides, int stride_size) {
+  const char* src = data;
+  for (unsigned int i = 0; i < num_strides; ++i, src += stride_size) {
+    if (!Write(src, size)) {
+      return false;
+    }
+  }
+  return true;
+}
+
+bool VsockClientConnection::Connect(unsigned int port, unsigned int cid) {
+  fd_ = SharedFD::VsockClient(cid, port, SOCK_STREAM);
+  if (!fd_->IsOpen()) {
+    LOG(ERROR) << "Failed to connect:" << fd_->StrError();
+  }
+  return fd_->IsOpen();
+}
+
+VsockServerConnection::~VsockServerConnection() { ServerShutdown(); }
+
+void VsockServerConnection::ServerShutdown() {
+  if (server_fd_->IsOpen()) {
+    LOG(INFO) << __FUNCTION__
+              << ": server fd status:" << server_fd_->StrError();
+    server_fd_->Shutdown(SHUT_RDWR);
+    server_fd_->Close();
+  }
+}
+
+bool VsockServerConnection::Connect(unsigned int port, unsigned int cid) {
+  if (!server_fd_->IsOpen()) {
+    server_fd_ = cuttlefish::SharedFD::VsockServer(port, SOCK_STREAM, cid);
+  }
+  if (server_fd_->IsOpen()) {
+    fd_ = SharedFD::Accept(*server_fd_);
+    return fd_->IsOpen();
+  } else {
+    return false;
+  }
+}
+
+}  // namespace cuttlefish
diff --git a/common/libs/utils/vsock_connection.h b/common/libs/utils/vsock_connection.h
new file mode 100644
index 0000000..1a16b2f
--- /dev/null
+++ b/common/libs/utils/vsock_connection.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+#pragma once
+
+#include <json/json.h>
+#include <functional>
+#include <future>
+#include <mutex>
+#include <vector>
+#include "common/libs/fs/shared_fd.h"
+
+namespace cuttlefish {
+
+class VsockConnection {
+ public:
+  virtual ~VsockConnection();
+  virtual bool Connect(unsigned int port, unsigned int cid) = 0;
+  virtual void Disconnect();
+  std::future<bool> ConnectAsync(unsigned int port, unsigned int cid);
+  void SetDisconnectCallback(std::function<void()> callback);
+
+  bool IsConnected() const;
+  bool DataAvailable() const;
+
+  int32_t Read();
+  bool Read(std::vector<char>& data);
+  std::vector<char> Read(size_t size);
+  std::future<std::vector<char>> ReadAsync(size_t size);
+
+  bool ReadMessage(std::vector<char>& data);
+  std::vector<char> ReadMessage();
+  std::future<std::vector<char>> ReadMessageAsync();
+  Json::Value ReadJsonMessage();
+  std::future<Json::Value> ReadJsonMessageAsync();
+
+  bool Write(int32_t data);
+  bool Write(const char* data, unsigned int size);
+  bool Write(const std::vector<char>& data);
+  bool WriteMessage(const std::string& data);
+  bool WriteMessage(const std::vector<char>& data);
+  bool WriteMessage(const Json::Value& data);
+  bool WriteStrides(const char* data, unsigned int size,
+                    unsigned int num_strides, int stride_size);
+
+ protected:
+  std::recursive_mutex read_mutex_;
+  std::recursive_mutex write_mutex_;
+  std::function<void()> disconnect_callback_;
+  SharedFD fd_;
+};
+
+class VsockClientConnection : public VsockConnection {
+ public:
+  bool Connect(unsigned int port, unsigned int cid) override;
+};
+
+class VsockServerConnection : public VsockConnection {
+ public:
+  virtual ~VsockServerConnection();
+  void ServerShutdown();
+  bool Connect(unsigned int port, unsigned int cid) override;
+
+ private:
+  SharedFD server_fd_;
+};
+
+}  // namespace cuttlefish
diff --git a/guest/commands/bt_vhci_forwarder/Android.bp b/guest/commands/bt_vhci_forwarder/Android.bp
index 72dd7c9..a9f63b1 100644
--- a/guest/commands/bt_vhci_forwarder/Android.bp
+++ b/guest/commands/bt_vhci_forwarder/Android.bp
@@ -13,8 +13,8 @@
         "liblog",
     ],
     static_libs: [
+        "h4_packetizer_lib",
         "libgflags",
-        "libbt-rootcanal",
     ],
     defaults: ["cuttlefish_guest_only"]
 }
diff --git a/guest/commands/bt_vhci_forwarder/main.cpp b/guest/commands/bt_vhci_forwarder/main.cpp
index 5d5fcce..d5c4622 100644
--- a/guest/commands/bt_vhci_forwarder/main.cpp
+++ b/guest/commands/bt_vhci_forwarder/main.cpp
@@ -128,7 +128,6 @@
         send(vhci_fd, HCI_ISODATA_PKT, raw_iso.data(), raw_iso.size());
       },
       []() { LOG(INFO) << "HCI socket device disconnected"; });
-
   while (true) {
     int ret = TEMP_FAILURE_RETRY(poll(fds, 2, -1));
     if (ret < 0) {
@@ -143,11 +142,15 @@
         PLOG(ERROR) << "vhci to virtio-console failed";
       }
     }
-
+    if (fds[1].revents & POLLHUP) {
+      LOG(ERROR) << "PollHUP";
+      usleep(50 * 1000);
+      continue;
+    }
     if (fds[1].revents & (POLLIN | POLLERR)) {
       // 'virtio-console to vhci' depends on H4Packetizer because vhci expects
       // full packet, but the data from virtio-console could be partial.
       h4.OnDataReady(virtio_fd);
     }
   }
-}
\ No newline at end of file
+}
diff --git a/guest/commands/setup_wifi/main.cpp b/guest/commands/setup_wifi/main.cpp
index 0a07c97..032f39e 100644
--- a/guest/commands/setup_wifi/main.cpp
+++ b/guest/commands/setup_wifi/main.cpp
@@ -139,9 +139,9 @@
 
   gflags::ParseCommandLineFlags(&argc, &argv, true);
 
-  int renamed_eth0 = RenameNetwork("eth0", "buried_eth0");
-  if (renamed_eth0 != 0) {
-    return renamed_eth0;
+  int renamed_eth2 = RenameNetwork("eth2", "buried_eth2");
+  if (renamed_eth2 != 0) {
+    return renamed_eth2;
   }
-  return CreateWifiWrapper("buried_eth0", "wlan0");
+  return CreateWifiWrapper("buried_eth2", "wlan0");
 }
diff --git a/guest/hals/audio/audio_hw.c b/guest/hals/audio/audio_hw.c
index 51110d7..c0c705a 100644
--- a/guest/hals/audio/audio_hw.c
+++ b/guest/hals/audio/audio_hw.c
@@ -738,8 +738,7 @@
 
 static int refine_input_parameters(uint32_t *sample_rate, audio_format_t *format, audio_channel_mask_t *channel_mask)
 {
-    // Crosvm only supports 48kHz streams for input
-    static const uint32_t sample_rates [] = {48000};
+    static const uint32_t sample_rates [] = {8000, 11025, 16000, 22050, 44100, 48000};
     static const int sample_rates_count = sizeof(sample_rates)/sizeof(uint32_t);
     bool inval = false;
     // Only PCM_16_bit is supported. If this is changed, stereo to mono drop
@@ -1339,6 +1338,11 @@
     return 0;
 }
 
+static int adev_set_audio_port_config(struct audio_hw_device *dev,
+                                      const struct audio_port_config *config) {
+  return 0;
+}
+
 static int adev_set_master_volume(struct audio_hw_device *dev, float volume)
 {
     return -ENOSYS;
@@ -1783,6 +1787,7 @@
     adev->device.set_parameters = adev_set_parameters;       // no op
     adev->device.get_parameters = adev_get_parameters;       // no op
     adev->device.get_audio_port = adev_get_audio_port;       // no op
+    adev->device.set_audio_port_config = adev_set_audio_port_config;  // no op
     adev->device.get_input_buffer_size = adev_get_input_buffer_size;
     adev->device.open_output_stream = adev_open_output_stream;
     adev->device.close_output_stream = adev_close_output_stream;
diff --git a/guest/hals/camera/Android.bp b/guest/hals/camera/Android.bp
new file mode 100644
index 0000000..7af1542
--- /dev/null
+++ b/guest/hals/camera/Android.bp
@@ -0,0 +1,84 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_binary {
+    name: "[email protected]",
+    defaults: ["hidl_defaults"],
+    proprietary: true,
+    relative_install_path: "hw",
+    srcs: ["external-service.cpp"],
+    compile_multilib: "first",
+    init_rc: ["[email protected]"],
+    shared_libs: [
+        "[email protected]",
+        "libbinder",
+        "libhidlbase",
+        "liblog",
+        "libutils",
+    ],
+}
+
+cc_library_shared {
+    name: "[email protected]",
+    defaults: ["hidl_defaults"],
+    proprietary: true,
+    relative_install_path: "hw",
+    srcs: [
+        "vsock_camera_provider_2_7.cpp",
+        "vsock_camera_device_3_4.cpp",
+        "vsock_camera_device_session_3_4.cpp",
+        "vsock_camera_metadata.cpp",
+        "vsock_camera_server.cpp",
+        "vsock_frame_provider.cpp",
+        "cached_stream_buffer.cpp",
+        "stream_buffer_cache.cpp",
+    ],
+    shared_libs: [
+        "[email protected]",
+        "[email protected]",
+        "[email protected]",
+        "[email protected]",
+        "[email protected]",
+        "[email protected]",
+        "[email protected]",
+        "[email protected]",
+        "[email protected]",
+        "[email protected]",
+        "[email protected]",
+        "[email protected]",
+        "[email protected]",
+        "[email protected]",
+        "[email protected]",
+        "[email protected]",
+        "[email protected]",
+        "[email protected]",
+        "[email protected]",
+        "[email protected]",
+        "[email protected]",
+        "libcamera_metadata",
+        "libcutils",
+        "libhardware",
+        "libhidlbase",
+        "liblog",
+        "libutils",
+        "libvsock_utils",
+        "libcuttlefish_fs",
+        "libjsoncpp",
+        "libyuv",
+        "libsync",
+        "libfmq",
+        "libgralloctypes",
+    ],
+    header_libs: [
+        "[email protected]_headers",
+        "[email protected]_headers",
+        "[email protected]_headers",
+        "[email protected]_headers",
+    ],
+    static_libs: [
+        "[email protected]",
+    ],
+    include_dirs: ["device/google/cuttlefish"],
+    export_include_dirs: ["."],
+}
diff --git a/guest/hals/camera/[email protected] b/guest/hals/camera/[email protected]
new file mode 100644
index 0000000..16bde0b
--- /dev/null
+++ b/guest/hals/camera/[email protected]
@@ -0,0 +1,11 @@
+service vendor.camera-provider-2-7-ext /vendor/bin/hw/[email protected]
+    interface [email protected]::ICameraProvider external/0
+    interface [email protected]::ICameraProvider external/0
+    interface [email protected]::ICameraProvider external/0
+    interface [email protected]::ICameraProvider external/0
+    class hal
+    user cameraserver
+    group audio camera input drmrpc
+    ioprio rt 4
+    capabilities SYS_NICE
+    task_profiles CameraServiceCapacity MaxPerformance
diff --git a/guest/hals/camera/cached_stream_buffer.cpp b/guest/hals/camera/cached_stream_buffer.cpp
new file mode 100644
index 0000000..ff692c7
--- /dev/null
+++ b/guest/hals/camera/cached_stream_buffer.cpp
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+#define LOG_TAG "CachedStreamBuffer"
+#include "cached_stream_buffer.h"
+#include <hardware/gralloc.h>
+#include <log/log.h>
+#include <sync/sync.h>
+
+namespace android::hardware::camera::device::V3_4::implementation {
+
+namespace {
+HandleImporter g_importer;
+}
+
+ReleaseFence::ReleaseFence(int fence_fd) : handle_(nullptr) {
+  if (fence_fd >= 0) {
+    handle_ = native_handle_create(/*numFds*/ 1, /*numInts*/ 0);
+    handle_->data[0] = fence_fd;
+  }
+}
+
+ReleaseFence::~ReleaseFence() {
+  if (handle_ != nullptr) {
+    native_handle_close(handle_);
+    native_handle_delete(handle_);
+  }
+}
+
+CachedStreamBuffer::CachedStreamBuffer()
+    : buffer_(nullptr), buffer_id_(0), stream_id_(0), acquire_fence_(-1) {}
+
+CachedStreamBuffer::CachedStreamBuffer(const StreamBuffer& buffer)
+    : buffer_(buffer.buffer.getNativeHandle()),
+      buffer_id_(buffer.bufferId),
+      stream_id_(buffer.streamId),
+      acquire_fence_(-1) {
+  g_importer.importBuffer(buffer_);
+  g_importer.importFence(buffer.acquireFence, acquire_fence_);
+}
+
+CachedStreamBuffer::CachedStreamBuffer(CachedStreamBuffer&& from) noexcept {
+  buffer_ = from.buffer_;
+  buffer_id_ = from.buffer_id_;
+  stream_id_ = from.stream_id_;
+  acquire_fence_ = from.acquire_fence_;
+  from.acquire_fence_ = -1;
+  from.buffer_ = nullptr;
+}
+
+CachedStreamBuffer& CachedStreamBuffer::operator=(
+    CachedStreamBuffer&& from) noexcept {
+  if (this != &from) {
+    buffer_ = from.buffer_;
+    buffer_id_ = from.buffer_id_;
+    stream_id_ = from.stream_id_;
+    acquire_fence_ = from.acquire_fence_;
+    from.acquire_fence_ = -1;
+    from.buffer_ = nullptr;
+  }
+  return *this;
+}
+
+CachedStreamBuffer::~CachedStreamBuffer() {
+  if (buffer_ != nullptr) {
+    g_importer.freeBuffer(buffer_);
+  }
+  g_importer.closeFence(acquire_fence_);
+}
+
+void CachedStreamBuffer::importFence(const native_handle_t* fence_handle) {
+  g_importer.closeFence(acquire_fence_);
+  g_importer.importFence(fence_handle, acquire_fence_);
+}
+
+YCbCrLayout CachedStreamBuffer::acquireAsYUV(int32_t width, int32_t height,
+                                             int timeout_ms) {
+  if (acquire_fence_ >= 0) {
+    if (sync_wait(acquire_fence_, timeout_ms)) {
+      ALOGW("%s: timeout while waiting acquire fence", __FUNCTION__);
+      return {};
+    } else {
+      ::close(acquire_fence_);
+      acquire_fence_ = -1;
+    }
+  }
+  IMapper::Rect region{0, 0, width, height};
+  return g_importer.lockYCbCr(buffer_, GRALLOC_USAGE_SW_WRITE_OFTEN, region);
+}
+
+void* CachedStreamBuffer::acquireAsBlob(int32_t size, int timeout_ms) {
+  if (acquire_fence_ >= 0) {
+    if (sync_wait(acquire_fence_, timeout_ms)) {
+      return nullptr;
+    } else {
+      ::close(acquire_fence_);
+      acquire_fence_ = -1;
+    }
+  }
+  return g_importer.lock(buffer_, GRALLOC_USAGE_SW_WRITE_OFTEN, size);
+}
+
+int CachedStreamBuffer::release() { return g_importer.unlock(buffer_); }
+
+}  // namespace android::hardware::camera::device::V3_4::implementation
diff --git a/guest/hals/camera/cached_stream_buffer.h b/guest/hals/camera/cached_stream_buffer.h
new file mode 100644
index 0000000..09a4047
--- /dev/null
+++ b/guest/hals/camera/cached_stream_buffer.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+#pragma once
+#include <android/hardware/camera/device/3.4/ICameraDeviceSession.h>
+#include "HandleImporter.h"
+
+namespace android::hardware::camera::device::V3_4::implementation {
+
+using ::android::hardware::camera::common::V1_0::helper::HandleImporter;
+using ::android::hardware::camera::device::V3_2::StreamBuffer;
+
+// Small wrapper for allocating/freeing native handles
+class ReleaseFence {
+ public:
+  ReleaseFence(int fence_fd);
+  ~ReleaseFence();
+
+  native_handle_t* handle() const { return handle_; }
+
+ private:
+  native_handle_t* handle_;
+};
+
+// CachedStreamBuffer holds a buffer of camera3 stream.
+class CachedStreamBuffer {
+ public:
+  CachedStreamBuffer();
+  CachedStreamBuffer(const StreamBuffer& buffer);
+  // Not copyable
+  CachedStreamBuffer(const CachedStreamBuffer&) = delete;
+  CachedStreamBuffer& operator=(const CachedStreamBuffer&) = delete;
+  // ...but movable
+  CachedStreamBuffer(CachedStreamBuffer&& from) noexcept;
+  CachedStreamBuffer& operator=(CachedStreamBuffer&& from) noexcept;
+
+  ~CachedStreamBuffer();
+
+  bool valid() const { return buffer_ != nullptr; }
+  uint64_t bufferId() const { return buffer_id_; }
+  int32_t streamId() const { return stream_id_; }
+  int acquireFence() const { return acquire_fence_; }
+
+  void importFence(const native_handle_t* fence_handle);
+  // Acquire methods wait first on acquire fence and then return pointers to
+  // data. Data is nullptr if the wait timed out
+  YCbCrLayout acquireAsYUV(int32_t width, int32_t height, int timeout_ms);
+  void* acquireAsBlob(int32_t size, int timeout_ms);
+  int release();
+
+ private:
+  buffer_handle_t buffer_;
+  uint64_t buffer_id_;
+  int32_t stream_id_;
+  int acquire_fence_;
+};
+
+}  // namespace android::hardware::camera::device::V3_4::implementation
diff --git a/guest/hals/camera/external-service.cpp b/guest/hals/camera/external-service.cpp
new file mode 100644
index 0000000..22e86a6
--- /dev/null
+++ b/guest/hals/camera/external-service.cpp
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2021 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.
+ */
+
+#define LOG_TAG "[email protected]"
+
+#include <android/hardware/camera/provider/2.7/ICameraProvider.h>
+#include <hidl/LegacySupport.h>
+
+#include <binder/ProcessState.h>
+
+using android::hardware::defaultPassthroughServiceImplementation;
+using android::hardware::camera::provider::V2_7::ICameraProvider;
+
+int main() {
+  ALOGI("External camera provider service is starting.");
+  // The camera HAL may communicate to other vendor components via
+  // /dev/vndbinder
+  android::ProcessState::initWithDriver("/dev/vndbinder");
+  return defaultPassthroughServiceImplementation<ICameraProvider>(
+      "external/0", /*maxThreads*/ 6);
+}
diff --git a/guest/hals/camera/manifest.xml b/guest/hals/camera/manifest.xml
new file mode 100644
index 0000000..4f23875
--- /dev/null
+++ b/guest/hals/camera/manifest.xml
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+<manifest version="1.0" type="device">
+    <hal format="hidl">
+        <name>android.hardware.camera.provider</name>
+        <transport>hwbinder</transport>
+        <version>2.7</version>
+        <interface>
+          <name>ICameraProvider</name>
+          <instance>external/0</instance>
+        </interface>
+    </hal>
+</manifest>
diff --git a/guest/hals/camera/stream_buffer_cache.cpp b/guest/hals/camera/stream_buffer_cache.cpp
new file mode 100644
index 0000000..dfa13cb
--- /dev/null
+++ b/guest/hals/camera/stream_buffer_cache.cpp
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+#include "stream_buffer_cache.h"
+#include <algorithm>
+
+namespace android::hardware::camera::device::V3_4::implementation {
+
+std::shared_ptr<CachedStreamBuffer> StreamBufferCache::get(uint64_t buffer_id) {
+  auto id_match =
+      [buffer_id](const std::shared_ptr<CachedStreamBuffer>& buffer) {
+        return buffer->bufferId() == buffer_id;
+      };
+  std::lock_guard<std::mutex> lock(mutex_);
+  auto found = std::find_if(cache_.begin(), cache_.end(), id_match);
+  return (found != cache_.end()) ? *found : nullptr;
+}
+
+void StreamBufferCache::remove(uint64_t buffer_id) {
+  auto id_match =
+      [&buffer_id](const std::shared_ptr<CachedStreamBuffer>& buffer) {
+        return buffer->bufferId() == buffer_id;
+      };
+  std::lock_guard<std::mutex> lock(mutex_);
+  cache_.erase(std::remove_if(cache_.begin(), cache_.end(), id_match));
+}
+
+void StreamBufferCache::update(const StreamBuffer& buffer) {
+  auto id = buffer.bufferId;
+  auto id_match = [id](const std::shared_ptr<CachedStreamBuffer>& buffer) {
+    return buffer->bufferId() == id;
+  };
+  std::lock_guard<std::mutex> lock(mutex_);
+  auto found = std::find_if(cache_.begin(), cache_.end(), id_match);
+  if (found == cache_.end()) {
+    cache_.emplace_back(std::make_shared<CachedStreamBuffer>(buffer));
+  } else {
+    (*found)->importFence(buffer.acquireFence);
+  }
+}
+
+void StreamBufferCache::clear() {
+  std::lock_guard<std::mutex> lock(mutex_);
+  cache_.clear();
+}
+
+void StreamBufferCache::removeStreamsExcept(std::set<int32_t> streams_to_keep) {
+  std::lock_guard<std::mutex> lock(mutex_);
+  for (auto it = cache_.begin(); it != cache_.end();) {
+    if (streams_to_keep.count((*it)->streamId()) == 0) {
+      it = cache_.erase(it);
+    } else {
+      it++;
+    }
+  }
+}
+
+}  // namespace android::hardware::camera::device::V3_4::implementation
diff --git a/guest/hals/camera/stream_buffer_cache.h b/guest/hals/camera/stream_buffer_cache.h
new file mode 100644
index 0000000..af57bc4
--- /dev/null
+++ b/guest/hals/camera/stream_buffer_cache.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+#pragma once
+#include <android/hardware/camera/device/3.4/ICameraDeviceSession.h>
+#include <mutex>
+#include <set>
+#include <vector>
+#include "cached_stream_buffer.h"
+
+namespace android::hardware::camera::device::V3_4::implementation {
+
+class StreamBufferCache {
+ public:
+  std::shared_ptr<CachedStreamBuffer> get(uint64_t buffer_id);
+  void remove(uint64_t buffer_id);
+  void update(const StreamBuffer& buffer);
+  void clear();
+  void removeStreamsExcept(std::set<int32_t> streams_to_keep = {});
+
+ private:
+  std::mutex mutex_;
+  std::vector<std::shared_ptr<CachedStreamBuffer>> cache_;
+};
+
+}  // namespace android::hardware::camera::device::V3_4::implementation
diff --git a/guest/hals/camera/vsock_camera_device_3_4.cpp b/guest/hals/camera/vsock_camera_device_3_4.cpp
new file mode 100644
index 0000000..310a9f8
--- /dev/null
+++ b/guest/hals/camera/vsock_camera_device_3_4.cpp
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+#define LOG_TAG "VsockCameraDevice"
+//#define LOG_NDEBUG 0
+#include <log/log.h>
+
+#include <algorithm>
+#include <array>
+#include "CameraMetadata.h"
+#include "android-base/macros.h"
+#include "include/convert.h"
+#include "vsock_camera_device_3_4.h"
+#include "vsock_camera_device_session_3_4.h"
+
+namespace android::hardware::camera::device::V3_4::implementation {
+
+VsockCameraDevice::VsockCameraDevice(
+    const std::string& id, const Settings& settings,
+    std::shared_ptr<cuttlefish::VsockConnection> connection)
+    : id_(id),
+      metadata_(settings.width, settings.height, settings.frame_rate),
+      connection_(connection),
+      is_open_(false) {
+  ALOGI("%s", __FUNCTION__);
+}
+
+VsockCameraDevice::~VsockCameraDevice() { ALOGI("%s", __FUNCTION__); }
+
+Return<void> VsockCameraDevice::getResourceCost(
+    ICameraDevice::getResourceCost_cb _hidl_cb) {
+  CameraResourceCost resCost;
+  resCost.resourceCost = 100;
+  _hidl_cb(Status::OK, resCost);
+  return Void();
+}
+
+Return<void> VsockCameraDevice::getCameraCharacteristics(
+    ICameraDevice::getCameraCharacteristics_cb _hidl_cb) {
+  V3_2::CameraMetadata hidl_vec;
+  const camera_metadata_t* metadata_ptr = metadata_.getAndLock();
+  V3_2::implementation::convertToHidl(metadata_ptr, &hidl_vec);
+  _hidl_cb(Status::OK, hidl_vec);
+  metadata_.unlock(metadata_ptr);
+  return Void();
+}
+
+Return<Status> VsockCameraDevice::setTorchMode(TorchMode) {
+  return Status::OPERATION_NOT_SUPPORTED;
+}
+
+Return<void> VsockCameraDevice::open(const sp<ICameraDeviceCallback>& callback,
+                                     ICameraDevice::open_cb _hidl_cb) {
+  if (callback == nullptr) {
+    ALOGE("%s: cannot open camera %s. callback is null!", __FUNCTION__,
+          id_.c_str());
+    _hidl_cb(Status::ILLEGAL_ARGUMENT, nullptr);
+    return Void();
+  }
+
+  bool was_open = is_open_.exchange(true);
+
+  if (was_open) {
+    ALOGE("%s: cannot open an already opened camera!", __FUNCTION__);
+    _hidl_cb(Status::CAMERA_IN_USE, nullptr);
+    return Void();
+  }
+  ALOGI("%s: Initializing device for camera %s", __FUNCTION__, id_.c_str());
+  frame_provider_ = std::make_shared<cuttlefish::VsockFrameProvider>();
+  frame_provider_->start(connection_, metadata_.getPreferredWidth(),
+                         metadata_.getPreferredHeight());
+  session_ = new VsockCameraDeviceSession(metadata_, frame_provider_, callback);
+  _hidl_cb(Status::OK, session_);
+  return Void();
+}
+
+Return<void> VsockCameraDevice::dumpState(
+    const ::android::hardware::hidl_handle& handle) {
+  if (handle.getNativeHandle() == nullptr) {
+    ALOGE("%s: handle must not be null", __FUNCTION__);
+    return Void();
+  }
+  if (handle->numFds != 1 || handle->numInts != 0) {
+    ALOGE("%s: handle must contain 1 FD and 0 integers! Got %d FDs and %d ints",
+          __FUNCTION__, handle->numFds, handle->numInts);
+    return Void();
+  }
+  int fd = handle->data[0];
+  dprintf(fd, "Camera:%s\n", id_.c_str());
+  return Void();
+}
+
+}  // namespace android::hardware::camera::device::V3_4::implementation
diff --git a/guest/hals/camera/vsock_camera_device_3_4.h b/guest/hals/camera/vsock_camera_device_3_4.h
new file mode 100644
index 0000000..9d92991
--- /dev/null
+++ b/guest/hals/camera/vsock_camera_device_3_4.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+#pragma once
+
+#include "CameraMetadata.h"
+
+#include <android/hardware/camera/device/3.2/ICameraDevice.h>
+#include <hidl/MQDescriptor.h>
+#include <hidl/Status.h>
+#include "vsock_camera_device_session_3_4.h"
+#include "vsock_camera_metadata.h"
+#include "vsock_connection.h"
+#include "vsock_frame_provider.h"
+
+#include <vector>
+
+namespace android::hardware::camera::device::V3_4::implementation {
+
+using namespace ::android::hardware::camera::device;
+using ::android::sp;
+using ::android::hardware::hidl_string;
+using ::android::hardware::hidl_vec;
+using ::android::hardware::Return;
+using ::android::hardware::Void;
+using ::android::hardware::camera::common::V1_0::CameraResourceCost;
+using ::android::hardware::camera::common::V1_0::Status;
+using ::android::hardware::camera::common::V1_0::TorchMode;
+using ::android::hardware::camera::device::V3_2::ICameraDevice;
+using ::android::hardware::camera::device::V3_2::ICameraDeviceCallback;
+
+class VsockCameraDevice : public ICameraDevice {
+ public:
+  using Settings = struct {
+    int32_t width;
+    int32_t height;
+    double frame_rate;
+  };
+
+  VsockCameraDevice(const std::string& id, const Settings& settings,
+                    std::shared_ptr<cuttlefish::VsockConnection> connection);
+  virtual ~VsockCameraDevice();
+
+  /* Methods from ::android::hardware::camera::device::V3_2::ICameraDevice
+   * follow. */
+  Return<void> getResourceCost(ICameraDevice::getResourceCost_cb _hidl_cb);
+  Return<void> getCameraCharacteristics(
+      ICameraDevice::getCameraCharacteristics_cb _hidl_cb);
+  Return<Status> setTorchMode(TorchMode);
+  Return<void> open(const sp<ICameraDeviceCallback>&, ICameraDevice::open_cb);
+  Return<void> dumpState(const ::android::hardware::hidl_handle&);
+  /* End of Methods from
+   * ::android::hardware::camera::device::V3_2::ICameraDevice */
+
+ private:
+  std::string id_;
+  VsockCameraMetadata metadata_;
+  std::shared_ptr<cuttlefish::VsockConnection> connection_;
+  std::shared_ptr<cuttlefish::VsockFrameProvider> frame_provider_;
+  std::atomic<bool> is_open_;
+  sp<VsockCameraDeviceSession> session_;
+};
+
+}  // namespace android::hardware::camera::device::V3_4::implementation
diff --git a/guest/hals/camera/vsock_camera_device_session_3_4.cpp b/guest/hals/camera/vsock_camera_device_session_3_4.cpp
new file mode 100644
index 0000000..9c0b597
--- /dev/null
+++ b/guest/hals/camera/vsock_camera_device_session_3_4.cpp
@@ -0,0 +1,574 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+#define LOG_TAG "VsockCameraDeviceSession"
+#include "vsock_camera_device_session_3_4.h"
+#include <hidl/Status.h>
+#include <include/convert.h>
+#include <inttypes.h>
+#include <libyuv.h>
+#include <log/log.h>
+#include "vsock_camera_metadata.h"
+
+// Partially copied from ExternalCameraDeviceSession
+namespace android::hardware::camera::device::V3_4::implementation {
+
+VsockCameraDeviceSession::VsockCameraDeviceSession(
+    VsockCameraMetadata camera_characteristics,
+    std::shared_ptr<cuttlefish::VsockFrameProvider> frame_provider,
+    const sp<ICameraDeviceCallback>& callback)
+    : camera_characteristics_(camera_characteristics),
+      frame_provider_(frame_provider),
+      callback_(callback) {
+  static constexpr size_t kMsgQueueSize = 256 * 1024;
+  request_queue_ =
+      std::make_unique<MessageQueue<uint8_t, kSynchronizedReadWrite>>(
+          kMsgQueueSize, false);
+  result_queue_ =
+      std::make_shared<MessageQueue<uint8_t, kSynchronizedReadWrite>>(
+          kMsgQueueSize, false);
+  unsigned int timeout_ms = 1000 / camera_characteristics.getPreferredFps();
+  process_requests_ = true;
+  request_processor_ =
+      std::thread([this, timeout_ms] { processRequestLoop(timeout_ms); });
+}
+
+VsockCameraDeviceSession::~VsockCameraDeviceSession() { close(); }
+
+Return<void> VsockCameraDeviceSession::constructDefaultRequestSettings(
+    V3_2::RequestTemplate type,
+    V3_2::ICameraDeviceSession::constructDefaultRequestSettings_cb _hidl_cb) {
+  auto frame_rate = camera_characteristics_.getPreferredFps();
+  auto metadata = VsockCameraRequestMetadata(frame_rate, type);
+  V3_2::CameraMetadata hidl_metadata;
+  Status status = metadata.isValid() ? Status::OK : Status::ILLEGAL_ARGUMENT;
+  if (metadata.isValid()) {
+    camera_metadata_t* metadata_ptr = metadata.release();
+    hidl_metadata.setToExternal((uint8_t*)metadata_ptr,
+                                get_camera_metadata_size(metadata_ptr));
+  }
+  _hidl_cb(status, hidl_metadata);
+  return Void();
+}
+
+Return<void> VsockCameraDeviceSession::getCaptureRequestMetadataQueue(
+    ICameraDeviceSession::getCaptureRequestMetadataQueue_cb _hidl_cb) {
+  _hidl_cb(*request_queue_->getDesc());
+  return Void();
+}
+
+Return<void> VsockCameraDeviceSession::getCaptureResultMetadataQueue(
+    ICameraDeviceSession::getCaptureResultMetadataQueue_cb _hidl_cb) {
+  _hidl_cb(*result_queue_->getDesc());
+  return Void();
+}
+
+Return<void> VsockCameraDeviceSession::configureStreams(
+    const V3_2::StreamConfiguration& streams,
+    ICameraDeviceSession::configureStreams_cb _hidl_cb) {
+  // common configureStreams operate with v3_2 config and v3_3
+  // streams so we need to "downcast" v3_3 streams to v3_2 streams
+  V3_2::HalStreamConfiguration out_v32;
+  V3_3::HalStreamConfiguration out_v33;
+
+  Status status = configureStreams(streams, &out_v33);
+  size_t size = out_v33.streams.size();
+  out_v32.streams.resize(size);
+  for (size_t i = 0; i < size; i++) {
+    out_v32.streams[i] = out_v33.streams[i].v3_2;
+  }
+  _hidl_cb(status, out_v32);
+  return Void();
+}
+
+Return<void> VsockCameraDeviceSession::configureStreams_3_3(
+    const V3_2::StreamConfiguration& streams,
+    ICameraDeviceSession::configureStreams_3_3_cb _hidl_cb) {
+  V3_3::HalStreamConfiguration out_v33;
+  Status status = configureStreams(streams, &out_v33);
+  _hidl_cb(status, out_v33);
+  return Void();
+}
+
+Return<void> VsockCameraDeviceSession::configureStreams_3_4(
+    const V3_4::StreamConfiguration& requestedConfiguration,
+    ICameraDeviceSession::configureStreams_3_4_cb _hidl_cb) {
+  // common configureStreams operate with v3_2 config and v3_3
+  // streams so we need to "downcast" v3_4 config to v3_2 and
+  // "upcast" v3_3 streams to v3_4 streams
+  V3_2::StreamConfiguration config_v32;
+  V3_3::HalStreamConfiguration out_v33;
+  V3_4::HalStreamConfiguration out_v34;
+
+  config_v32.operationMode = requestedConfiguration.operationMode;
+  config_v32.streams.resize(requestedConfiguration.streams.size());
+  for (size_t i = 0; i < config_v32.streams.size(); i++) {
+    config_v32.streams[i] = requestedConfiguration.streams[i].v3_2;
+  }
+  max_blob_size_ = getBlobSize(requestedConfiguration);
+  Status status = configureStreams(config_v32, &out_v33);
+
+  out_v34.streams.resize(out_v33.streams.size());
+  for (size_t i = 0; i < out_v34.streams.size(); i++) {
+    out_v34.streams[i].v3_3 = out_v33.streams[i];
+  }
+  _hidl_cb(status, out_v34);
+  return Void();
+}
+
+Return<void> VsockCameraDeviceSession::processCaptureRequest(
+    const hidl_vec<CaptureRequest>& requests,
+    const hidl_vec<BufferCache>& cachesToRemove,
+    ICameraDeviceSession::processCaptureRequest_cb _hidl_cb) {
+  updateBufferCaches(cachesToRemove);
+
+  uint32_t count;
+  Status s = Status::OK;
+  for (count = 0; count < requests.size(); count++) {
+    s = processOneCaptureRequest(requests[count]);
+    if (s != Status::OK) {
+      break;
+    }
+  }
+
+  _hidl_cb(s, count);
+  return Void();
+}
+
+Return<void> VsockCameraDeviceSession::processCaptureRequest_3_4(
+    const hidl_vec<V3_4::CaptureRequest>& requests,
+    const hidl_vec<V3_2::BufferCache>& cachesToRemove,
+    ICameraDeviceSession::processCaptureRequest_3_4_cb _hidl_cb) {
+  updateBufferCaches(cachesToRemove);
+
+  uint32_t count;
+  Status s = Status::OK;
+  for (count = 0; count < requests.size(); count++) {
+    s = processOneCaptureRequest(requests[count].v3_2);
+    if (s != Status::OK) {
+      break;
+    }
+  }
+
+  _hidl_cb(s, count);
+  return Void();
+}
+
+Return<Status> VsockCameraDeviceSession::flush() {
+  auto timeout = std::chrono::seconds(1);
+  std::unique_lock<std::mutex> lock(request_mutex_);
+  flushing_requests_ = true;
+  auto is_empty = [this] { return pending_requests_.empty(); };
+  if (!queue_empty_.wait_for(lock, timeout, is_empty)) {
+    ALOGE("Flush timeout - %zu pending requests", pending_requests_.size());
+  }
+  flushing_requests_ = false;
+  return Status::OK;
+}
+
+Return<void> VsockCameraDeviceSession::close() {
+  process_requests_ = false;
+  if (request_processor_.joinable()) {
+    request_processor_.join();
+  }
+  frame_provider_->stop();
+  buffer_cache_.clear();
+  ALOGI("%s", __FUNCTION__);
+  return Void();
+}
+
+using ::android::hardware::graphics::common::V1_0::BufferUsage;
+using ::android::hardware::graphics::common::V1_0::PixelFormat;
+Status VsockCameraDeviceSession::configureStreams(
+    const V3_2::StreamConfiguration& config,
+    V3_3::HalStreamConfiguration* out) {
+  Status status = isStreamConfigurationSupported(config);
+  if (status != Status::OK) {
+    return status;
+  }
+  updateStreamInfo(config);
+  out->streams.resize(config.streams.size());
+  for (size_t i = 0; i < config.streams.size(); i++) {
+    out->streams[i].overrideDataSpace = config.streams[i].dataSpace;
+    out->streams[i].v3_2.id = config.streams[i].id;
+    out->streams[i].v3_2.producerUsage =
+        config.streams[i].usage | BufferUsage::CPU_WRITE_OFTEN;
+    out->streams[i].v3_2.consumerUsage = 0;
+    out->streams[i].v3_2.maxBuffers = 2;
+    out->streams[i].v3_2.overrideFormat =
+        config.streams[i].format == PixelFormat::IMPLEMENTATION_DEFINED
+            ? PixelFormat::YCBCR_420_888
+            : config.streams[i].format;
+  }
+  return Status::OK;
+}
+
+using ::android::hardware::camera::device::V3_2::StreamRotation;
+using ::android::hardware::camera::device::V3_2::StreamType;
+Status VsockCameraDeviceSession::isStreamConfigurationSupported(
+    const V3_2::StreamConfiguration& config) {
+  camera_metadata_entry device_supported_streams = camera_characteristics_.find(
+      ANDROID_SCALER_AVAILABLE_STREAM_CONFIGURATIONS);
+  int32_t stall_stream_count = 0;
+  int32_t stream_count = 0;
+  for (const auto& stream : config.streams) {
+    if (stream.rotation != StreamRotation::ROTATION_0) {
+      ALOGE("Unsupported rotation enum value %d", stream.rotation);
+      return Status::ILLEGAL_ARGUMENT;
+    }
+    if (stream.streamType == StreamType::INPUT) {
+      ALOGE("Input stream not supported");
+      return Status::ILLEGAL_ARGUMENT;
+    }
+    bool is_supported = false;
+    // check pixel format and dimensions against camera metadata
+    for (int i = 0; i + 4 <= device_supported_streams.count; i += 4) {
+      auto format =
+          static_cast<PixelFormat>(device_supported_streams.data.i32[i]);
+      int32_t width = device_supported_streams.data.i32[i + 1];
+      int32_t height = device_supported_streams.data.i32[i + 2];
+      if (stream.format == format && stream.width == width &&
+          stream.height == height) {
+        is_supported = true;
+        break;
+      }
+    }
+    if (!is_supported) {
+      ALOGE("Unsupported format %d (%dx%d)", stream.format, stream.width,
+            stream.height);
+      return Status::ILLEGAL_ARGUMENT;
+    }
+    if (stream.format == PixelFormat::BLOB) {
+      stall_stream_count++;
+    } else {
+      stream_count++;
+    }
+  }
+  camera_metadata_entry device_stream_counts =
+      camera_characteristics_.find(ANDROID_REQUEST_MAX_NUM_OUTPUT_STREAMS);
+  static constexpr auto stream_index = 1;
+  auto expected_stream_count = device_stream_counts.count > stream_index
+                                   ? device_stream_counts.data.i32[stream_index]
+                                   : 0;
+  if (stream_count > expected_stream_count) {
+    ALOGE("Too many processed streams (expect <= %d, got %d)",
+          expected_stream_count, stream_count);
+    return Status::ILLEGAL_ARGUMENT;
+  }
+  static constexpr auto stall_stream_index = 2;
+  expected_stream_count =
+      device_stream_counts.count > stall_stream_index
+          ? device_stream_counts.data.i32[stall_stream_index]
+          : 0;
+  if (stall_stream_count > expected_stream_count) {
+    ALOGE("Too many stall streams (expect <= %d, got %d)",
+          expected_stream_count, stall_stream_count);
+    return Status::ILLEGAL_ARGUMENT;
+  }
+  return Status::OK;
+}
+
+unsigned int VsockCameraDeviceSession::getBlobSize(
+    const V3_4::StreamConfiguration& requestedConfiguration) {
+  camera_metadata_entry jpeg_entry =
+      camera_characteristics_.find(ANDROID_JPEG_MAX_SIZE);
+  unsigned int blob_size = jpeg_entry.count > 0 ? jpeg_entry.data.i32[0] : 0;
+  for (auto& stream : requestedConfiguration.streams) {
+    if (stream.v3_2.format == PixelFormat::BLOB) {
+      if (stream.bufferSize < blob_size) {
+        blob_size = stream.bufferSize;
+      }
+    }
+  }
+  return blob_size;
+}
+
+void VsockCameraDeviceSession::updateBufferCaches(
+    const hidl_vec<BufferCache>& to_remove) {
+  for (auto& cache : to_remove) {
+    buffer_cache_.remove(cache.bufferId);
+  }
+}
+
+void VsockCameraDeviceSession::updateStreamInfo(
+    const V3_2::StreamConfiguration& config) {
+  std::set<int32_t> stream_ids;
+  for (const auto& stream : config.streams) {
+    stream_cache_[stream.id] = stream;
+    stream_ids.emplace(stream.id);
+  }
+  buffer_cache_.removeStreamsExcept(stream_ids);
+}
+
+Status VsockCameraDeviceSession::processOneCaptureRequest(
+    const CaptureRequest& request) {
+  const camera_metadata_t* request_settings = nullptr;
+  V3_2::CameraMetadata hidl_settings;
+  if (request.fmqSettingsSize > 0) {
+    if (!getRequestSettingsFmq(request.fmqSettingsSize, hidl_settings)) {
+      ALOGE("%s: Could not read capture request settings from fmq!",
+            __FUNCTION__);
+      return Status::ILLEGAL_ARGUMENT;
+    } else if (!V3_2::implementation::convertFromHidl(hidl_settings,
+                                                      &request_settings)) {
+      ALOGE("%s: fmq request settings metadata is corrupt!", __FUNCTION__);
+      return Status::ILLEGAL_ARGUMENT;
+    }
+  } else if (!V3_2::implementation::convertFromHidl(request.settings,
+                                                    &request_settings)) {
+    ALOGE("%s: request settings metadata is corrupt!", __FUNCTION__);
+    return Status::ILLEGAL_ARGUMENT;
+  }
+  if (request_settings != nullptr) {
+    // Update request settings. This must happen on first request
+    std::lock_guard<std::mutex> lock(settings_mutex_);
+    latest_request_settings_ = request_settings;
+  } else if (latest_request_settings_.isEmpty()) {
+    ALOGE("%s: Undefined capture request settings!", __FUNCTION__);
+    return Status::ILLEGAL_ARGUMENT;
+  }
+
+  std::vector<uint64_t> buffer_ids;
+  for (size_t i = 0; i < request.outputBuffers.size(); i++) {
+    buffer_cache_.update(request.outputBuffers[i]);
+    buffer_ids.emplace_back(request.outputBuffers[i].bufferId);
+  }
+  std::lock_guard<std::mutex> lock(settings_mutex_);
+  ReadVsockRequest request_to_process = {
+      .buffer_ids = buffer_ids,
+      .frame_number = request.frameNumber,
+      .timestamp = 0,
+      .settings = latest_request_settings_,
+      .buffer_count = static_cast<uint32_t>(buffer_ids.size())};
+  putRequestToQueue(request_to_process);
+  return Status::OK;
+}
+
+bool VsockCameraDeviceSession::getRequestSettingsFmq(
+    uint64_t size, V3_2::CameraMetadata& hidl_settings) {
+  hidl_settings.resize(size);
+  return request_queue_->read(hidl_settings.data(), size);
+}
+
+bool VsockCameraDeviceSession::getRequestFromQueue(ReadVsockRequest& req,
+                                                   unsigned int timeout_ms) {
+  auto timeout = std::chrono::milliseconds(timeout_ms);
+  std::unique_lock<std::mutex> lock(request_mutex_);
+  auto not_empty = [this] { return !pending_requests_.empty(); };
+  if (request_available_.wait_for(lock, timeout, not_empty)) {
+    req = pending_requests_.top();
+    pending_requests_.pop();
+    return true;
+  }
+  queue_empty_.notify_one();
+  return false;
+}
+
+void VsockCameraDeviceSession::putRequestToQueue(
+    const ReadVsockRequest& request) {
+  std::lock_guard<std::mutex> lock(request_mutex_);
+  pending_requests_.push(request);
+  request_available_.notify_one();
+}
+
+void VsockCameraDeviceSession::fillCaptureResult(
+    common::V1_0::helper::CameraMetadata& md, nsecs_t timestamp) {
+  const uint8_t af_state = ANDROID_CONTROL_AF_STATE_INACTIVE;
+  md.update(ANDROID_CONTROL_AF_STATE, &af_state, 1);
+
+  const uint8_t aeState = ANDROID_CONTROL_AE_STATE_CONVERGED;
+  md.update(ANDROID_CONTROL_AE_STATE, &aeState, 1);
+
+  const uint8_t ae_lock = ANDROID_CONTROL_AE_LOCK_OFF;
+  md.update(ANDROID_CONTROL_AE_LOCK, &ae_lock, 1);
+
+  const uint8_t awbState = ANDROID_CONTROL_AWB_STATE_CONVERGED;
+  md.update(ANDROID_CONTROL_AWB_STATE, &awbState, 1);
+
+  const uint8_t awbLock = ANDROID_CONTROL_AWB_LOCK_OFF;
+  md.update(ANDROID_CONTROL_AWB_LOCK, &awbLock, 1);
+
+  const uint8_t flashState = ANDROID_FLASH_STATE_UNAVAILABLE;
+  md.update(ANDROID_FLASH_STATE, &flashState, 1);
+
+  const uint8_t requestPipelineMaxDepth = 4;
+  md.update(ANDROID_REQUEST_PIPELINE_DEPTH, &requestPipelineMaxDepth, 1);
+
+  camera_metadata_entry active_array_size =
+      camera_characteristics_.find(ANDROID_SENSOR_INFO_ACTIVE_ARRAY_SIZE);
+  md.update(ANDROID_SCALER_CROP_REGION, active_array_size.data.i32, 4);
+
+  md.update(ANDROID_SENSOR_TIMESTAMP, &timestamp, 1);
+
+  const uint8_t lensShadingMapMode =
+      ANDROID_STATISTICS_LENS_SHADING_MAP_MODE_OFF;
+  md.update(ANDROID_STATISTICS_LENS_SHADING_MAP_MODE, &lensShadingMapMode, 1);
+
+  const uint8_t sceneFlicker = ANDROID_STATISTICS_SCENE_FLICKER_NONE;
+  md.update(ANDROID_STATISTICS_SCENE_FLICKER, &sceneFlicker, 1);
+}
+
+using ::android::hardware::camera::device::V3_2::MsgType;
+using ::android::hardware::camera::device::V3_2::NotifyMsg;
+void VsockCameraDeviceSession::notifyShutter(uint32_t frame_number,
+                                             nsecs_t timestamp) {
+  NotifyMsg msg;
+  msg.type = MsgType::SHUTTER;
+  msg.msg.shutter.frameNumber = frame_number;
+  msg.msg.shutter.timestamp = timestamp;
+  callback_->notify({msg});
+}
+
+void VsockCameraDeviceSession::notifyError(uint32_t frame_number,
+                                           int32_t stream_id, ErrorCode code) {
+  NotifyMsg msg;
+  msg.type = MsgType::ERROR;
+  msg.msg.error.frameNumber = frame_number;
+  msg.msg.error.errorStreamId = stream_id;
+  msg.msg.error.errorCode = code;
+  callback_->notify({msg});
+}
+
+void VsockCameraDeviceSession::tryWriteFmqResult(V3_2::CaptureResult& result) {
+  result.fmqResultSize = 0;
+  if (result_queue_->availableToWrite() == 0 || result.result.size() == 0) {
+    return;
+  }
+  if (result_queue_->write(result.result.data(), result.result.size())) {
+    result.fmqResultSize = result.result.size();
+    result.result.resize(0);
+  }
+}
+
+using ::android::hardware::camera::device::V3_2::BufferStatus;
+using ::android::hardware::graphics::common::V1_0::PixelFormat;
+void VsockCameraDeviceSession::processRequestLoop(
+    unsigned int wait_timeout_ms) {
+  while (process_requests_.load()) {
+    ReadVsockRequest request;
+    if (!getRequestFromQueue(request, wait_timeout_ms)) {
+      continue;
+    }
+    if (!frame_provider_->isRunning()) {
+      notifyError(request.frame_number, -1, ErrorCode::ERROR_DEVICE);
+      break;
+    }
+    frame_provider_->waitYUVFrame(wait_timeout_ms);
+    nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
+    if (request.timestamp == 0) {
+      request.timestamp = now;
+      notifyShutter(request.frame_number, request.timestamp);
+    }
+    std::vector<ReleaseFence> release_fences;
+    std::vector<StreamBuffer> result_buffers;
+    std::vector<uint64_t> pending_buffers;
+    bool request_ok = true;
+    for (auto buffer_id : request.buffer_ids) {
+      auto buffer = buffer_cache_.get(buffer_id);
+      auto stream_id = buffer ? buffer->streamId() : -1;
+      if (!buffer || stream_cache_.count(stream_id) == 0) {
+        ALOGE("%s: Invalid buffer", __FUNCTION__);
+        notifyError(request.frame_number, -1, ErrorCode::ERROR_REQUEST);
+        request_ok = false;
+        break;
+      }
+      bool has_result = false;
+      auto stream = stream_cache_[stream_id];
+      if (flushing_requests_.load()) {
+        has_result = false;
+        release_fences.emplace_back(buffer->acquireFence());
+      } else if (stream.format == PixelFormat::YCBCR_420_888 ||
+                 stream.format == PixelFormat::IMPLEMENTATION_DEFINED) {
+        auto dst_yuv =
+            buffer->acquireAsYUV(stream.width, stream.height, wait_timeout_ms);
+        has_result =
+            frame_provider_->copyYUVFrame(stream.width, stream.height, dst_yuv);
+        release_fences.emplace_back(buffer->release());
+      } else if (stream.format == PixelFormat::BLOB) {
+        auto time_elapsed = now - request.timestamp;
+        if (time_elapsed == 0) {
+          frame_provider_->requestJpeg();
+          pending_buffers.push_back(buffer_id);
+          continue;
+        } else if (frame_provider_->jpegPending()) {
+          static constexpr auto kMaxWaitNs = 2000000000L;
+          if (time_elapsed < kMaxWaitNs) {
+            pending_buffers.push_back(buffer_id);
+            continue;
+          }
+          ALOGE("%s: Blob request timed out after %" PRId64 "ms", __FUNCTION__,
+                ns2ms(time_elapsed));
+          frame_provider_->cancelJpegRequest();
+          has_result = false;
+          release_fences.emplace_back(buffer->acquireFence());
+          notifyError(request.frame_number, buffer->streamId(),
+                      ErrorCode::ERROR_BUFFER);
+        } else {
+          ALOGI("%s: Blob ready - capture duration=%" PRId64 "ms", __FUNCTION__,
+                ns2ms(time_elapsed));
+          auto dst_blob =
+              buffer->acquireAsBlob(max_blob_size_, wait_timeout_ms);
+          has_result = frame_provider_->copyJpegData(max_blob_size_, dst_blob);
+          release_fences.emplace_back(buffer->release());
+        }
+      } else {
+        ALOGE("%s: Format %d not supported", __FUNCTION__, stream.format);
+        has_result = false;
+        release_fences.emplace_back(buffer->acquireFence());
+        notifyError(request.frame_number, buffer->streamId(),
+                    ErrorCode::ERROR_BUFFER);
+      }
+      result_buffers.push_back(
+          {.streamId = buffer->streamId(),
+           .bufferId = buffer->bufferId(),
+           .buffer = nullptr,
+           .status = has_result ? BufferStatus::OK : BufferStatus::ERROR,
+           .releaseFence = release_fences.back().handle()});
+    }
+    if (!request_ok) {
+      continue;
+    }
+
+    V3_2::CaptureResult result;
+    bool results_filled = request.settings.exists(ANDROID_SENSOR_TIMESTAMP);
+    if (!results_filled) {
+      fillCaptureResult(request.settings, request.timestamp);
+      const camera_metadata_t* metadata = request.settings.getAndLock();
+      V3_2::implementation::convertToHidl(metadata, &result.result);
+      request.settings.unlock(metadata);
+      tryWriteFmqResult(result);
+    }
+    if (!result_buffers.empty() || !results_filled) {
+      result.frameNumber = request.frame_number;
+      result.partialResult = !results_filled ? 1 : 0;
+      result.inputBuffer.streamId = -1;
+      result.outputBuffers = result_buffers;
+      std::vector<V3_2::CaptureResult> results{result};
+      auto status = callback_->processCaptureResult(results);
+      release_fences.clear();
+      if (!status.isOk()) {
+        ALOGE("%s: processCaptureResult error: %s", __FUNCTION__,
+              status.description().c_str());
+      }
+    }
+    if (!pending_buffers.empty()) {
+      // some buffers still pending
+      request.buffer_ids = pending_buffers;
+      putRequestToQueue(request);
+    }
+  }
+}
+
+}  // namespace android::hardware::camera::device::V3_4::implementation
diff --git a/guest/hals/camera/vsock_camera_device_session_3_4.h b/guest/hals/camera/vsock_camera_device_session_3_4.h
new file mode 100644
index 0000000..2bafa75
--- /dev/null
+++ b/guest/hals/camera/vsock_camera_device_session_3_4.h
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+#pragma once
+#include <android/hardware/camera/device/3.2/ICameraDevice.h>
+#include <android/hardware/camera/device/3.4/ICameraDeviceSession.h>
+#include <android/hardware/graphics/mapper/2.0/IMapper.h>
+#include <fmq/MessageQueue.h>
+#include <queue>
+#include <thread>
+#include "stream_buffer_cache.h"
+#include "vsock_camera_metadata.h"
+#include "vsock_frame_provider.h"
+
+namespace android::hardware::camera::device::V3_4::implementation {
+using ::android::sp;
+using ::android::hardware::hidl_vec;
+using ::android::hardware::kSynchronizedReadWrite;
+using ::android::hardware::MessageQueue;
+using ::android::hardware::Return;
+using ::android::hardware::camera::common::V1_0::Status;
+using ::android::hardware::camera::device::V3_2::BufferCache;
+using ::android::hardware::camera::device::V3_2::CaptureRequest;
+using ::android::hardware::camera::device::V3_2::ErrorCode;
+using ::android::hardware::camera::device::V3_2::ICameraDeviceCallback;
+using ::android::hardware::camera::device::V3_2::RequestTemplate;
+using ::android::hardware::camera::device::V3_2::Stream;
+using ::android::hardware::camera::device::V3_4::ICameraDeviceSession;
+using ::android::hardware::camera::device::V3_4::StreamConfiguration;
+using ::android::hardware::graphics::mapper::V2_0::YCbCrLayout;
+
+class VsockCameraDeviceSession : public ICameraDeviceSession {
+ public:
+  VsockCameraDeviceSession(
+      VsockCameraMetadata camera_characteristics,
+      std::shared_ptr<cuttlefish::VsockFrameProvider> frame_provider,
+      const sp<ICameraDeviceCallback>& callback);
+
+  ~VsockCameraDeviceSession();
+
+  Return<void> constructDefaultRequestSettings(
+      RequestTemplate,
+      ICameraDeviceSession::constructDefaultRequestSettings_cb _hidl_cb);
+
+  Return<void> configureStreams(const V3_2::StreamConfiguration&,
+                                ICameraDeviceSession::configureStreams_cb);
+
+  Return<void> configureStreams_3_3(
+      const V3_2::StreamConfiguration&,
+      ICameraDeviceSession::configureStreams_3_3_cb);
+
+  Return<void> configureStreams_3_4(
+      const V3_4::StreamConfiguration& requestedConfiguration,
+      ICameraDeviceSession::configureStreams_3_4_cb _hidl_cb);
+
+  Return<void> getCaptureRequestMetadataQueue(
+      ICameraDeviceSession::getCaptureRequestMetadataQueue_cb);
+
+  Return<void> getCaptureResultMetadataQueue(
+      ICameraDeviceSession::getCaptureResultMetadataQueue_cb);
+
+  Return<void> processCaptureRequest(
+      const hidl_vec<CaptureRequest>&, const hidl_vec<BufferCache>&,
+      ICameraDeviceSession::processCaptureRequest_cb);
+
+  Return<Status> flush();
+  Return<void> close();
+
+  Return<void> processCaptureRequest_3_4(
+      const hidl_vec<V3_4::CaptureRequest>& requests,
+      const hidl_vec<V3_2::BufferCache>& cachesToRemove,
+      ICameraDeviceSession::processCaptureRequest_3_4_cb _hidl_cb);
+
+ private:
+  struct ReadVsockRequest {
+    std::vector<uint64_t> buffer_ids;
+    uint32_t frame_number;
+    nsecs_t timestamp;
+    common::V1_0::helper::CameraMetadata settings;
+    uint32_t buffer_count;
+  };
+  struct VsockRequestComparator {
+    bool operator()(const ReadVsockRequest& lhs, const ReadVsockRequest& rhs) {
+      return lhs.frame_number > rhs.frame_number;
+    }
+  };
+  void updateBufferCaches(const hidl_vec<BufferCache>& to_remove);
+  Status configureStreams(const V3_2::StreamConfiguration& config,
+                          V3_3::HalStreamConfiguration* out);
+  unsigned int getBlobSize(
+      const V3_4::StreamConfiguration& requestedConfiguration);
+  Status isStreamConfigurationSupported(
+      const V3_2::StreamConfiguration& config);
+  void updateStreamInfo(const V3_2::StreamConfiguration& config);
+  Status processOneCaptureRequest(const CaptureRequest& request);
+  bool getRequestSettingsFmq(uint64_t size,
+                             V3_2::CameraMetadata& hidl_settings);
+  void processRequestLoop(unsigned int timeout);
+  bool getRequestFromQueue(ReadVsockRequest& request, unsigned int timeout_ms);
+  void putRequestToQueue(const ReadVsockRequest& request);
+  void fillCaptureResult(common::V1_0::helper::CameraMetadata& md,
+                         nsecs_t timestamp);
+  void notifyShutter(uint32_t frame_number, nsecs_t timestamp);
+  void notifyError(uint32_t frame_number, int32_t stream_id, ErrorCode code);
+  void tryWriteFmqResult(V3_2::CaptureResult& result);
+  VsockCameraMetadata camera_characteristics_;
+  std::shared_ptr<cuttlefish::VsockFrameProvider> frame_provider_;
+  const sp<ICameraDeviceCallback> callback_;
+  std::unique_ptr<MessageQueue<uint8_t, kSynchronizedReadWrite>> request_queue_;
+  std::shared_ptr<MessageQueue<uint8_t, kSynchronizedReadWrite>> result_queue_;
+  std::mutex settings_mutex_;
+  common::V1_0::helper::CameraMetadata latest_request_settings_;
+
+  StreamBufferCache buffer_cache_;
+  std::map<int32_t, Stream> stream_cache_;
+
+  std::mutex request_mutex_;
+  std::condition_variable request_available_;
+  std::condition_variable queue_empty_;
+  std::priority_queue<ReadVsockRequest, std::vector<ReadVsockRequest>,
+                      VsockRequestComparator>
+      pending_requests_;
+  std::thread request_processor_;
+  std::atomic<bool> process_requests_;
+  std::atomic<bool> flushing_requests_;
+
+  unsigned int max_blob_size_;
+};
+
+}  // namespace android::hardware::camera::device::V3_4::implementation
diff --git a/guest/hals/camera/vsock_camera_metadata.cpp b/guest/hals/camera/vsock_camera_metadata.cpp
new file mode 100644
index 0000000..2e92970
--- /dev/null
+++ b/guest/hals/camera/vsock_camera_metadata.cpp
@@ -0,0 +1,374 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+#include "vsock_camera_metadata.h"
+
+#include <hardware/camera3.h>
+#include <utils/misc.h>
+#include <vector>
+
+namespace android::hardware::camera::device::V3_4::implementation {
+
+namespace {
+// Mostly copied from ExternalCameraDevice
+const uint8_t kHardwarelevel = ANDROID_INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL;
+const uint8_t kAberrationMode = ANDROID_COLOR_CORRECTION_ABERRATION_MODE_OFF;
+const uint8_t kAvailableAberrationModes[] = {
+    ANDROID_COLOR_CORRECTION_ABERRATION_MODE_OFF};
+const int32_t kExposureCompensation = 0;
+const uint8_t kAntibandingMode = ANDROID_CONTROL_AE_ANTIBANDING_MODE_AUTO;
+const int32_t kControlMaxRegions[] = {/*AE*/ 0, /*AWB*/ 0, /*AF*/ 0};
+const uint8_t kVideoStabilizationMode =
+    ANDROID_CONTROL_VIDEO_STABILIZATION_MODE_OFF;
+const uint8_t kAwbAvailableMode = ANDROID_CONTROL_AWB_MODE_AUTO;
+const uint8_t kAePrecaptureTrigger = ANDROID_CONTROL_AE_PRECAPTURE_TRIGGER_IDLE;
+const uint8_t kAeAvailableMode = ANDROID_CONTROL_AE_MODE_ON;
+const uint8_t kAvailableFffect = ANDROID_CONTROL_EFFECT_MODE_OFF;
+const uint8_t kControlMode = ANDROID_CONTROL_MODE_AUTO;
+const uint8_t kControlAvailableModes[] = {ANDROID_CONTROL_MODE_OFF,
+                                          ANDROID_CONTROL_MODE_AUTO};
+const uint8_t kEdgeMode = ANDROID_EDGE_MODE_OFF;
+const uint8_t kFlashInfo = ANDROID_FLASH_INFO_AVAILABLE_FALSE;
+const uint8_t kFlashMode = ANDROID_FLASH_MODE_OFF;
+const uint8_t kHotPixelMode = ANDROID_HOT_PIXEL_MODE_OFF;
+const uint8_t kJpegQuality = 90;
+const int32_t kJpegOrientation = 0;
+const int32_t kThumbnailSize[] = {240, 180};
+const int32_t kJpegAvailableThumbnailSizes[] = {0, 0, 240, 180};
+const uint8_t kFocusDistanceCalibration =
+    ANDROID_LENS_INFO_FOCUS_DISTANCE_CALIBRATION_UNCALIBRATED;
+const uint8_t kOpticalStabilizationMode =
+    ANDROID_LENS_OPTICAL_STABILIZATION_MODE_OFF;
+const uint8_t kFacing = ANDROID_LENS_FACING_EXTERNAL;
+const float kLensMinFocusDistance = 0.0f;
+const uint8_t kNoiseReductionMode = ANDROID_NOISE_REDUCTION_MODE_OFF;
+const int32_t kPartialResultCount = 1;
+const uint8_t kRequestPipelineMaxDepth = 4;
+const int32_t kRequestMaxNumInputStreams = 0;
+const float kScalerAvailableMaxDigitalZoom[] = {1};
+const uint8_t kCroppingType = ANDROID_SCALER_CROPPING_TYPE_CENTER_ONLY;
+const int32_t kTestPatternMode = ANDROID_SENSOR_TEST_PATTERN_MODE_OFF;
+const int32_t kTestPatternModes[] = {ANDROID_SENSOR_TEST_PATTERN_MODE_OFF};
+const uint8_t kTimestampSource = ANDROID_SENSOR_INFO_TIMESTAMP_SOURCE_UNKNOWN;
+const int32_t kOrientation = 0;
+const uint8_t kAvailableShadingMode = ANDROID_SHADING_MODE_OFF;
+const uint8_t kFaceDetectMode = ANDROID_STATISTICS_FACE_DETECT_MODE_OFF;
+const int32_t kMaxFaceCount = 0;
+const uint8_t kAvailableHotpixelMode =
+    ANDROID_STATISTICS_HOT_PIXEL_MAP_MODE_OFF;
+const uint8_t kLensShadingMapMode =
+    ANDROID_STATISTICS_LENS_SHADING_MAP_MODE_OFF;
+const int32_t kMaxLatency = ANDROID_SYNC_MAX_LATENCY_UNKNOWN;
+const int32_t kControlAeCompensationRange[] = {0, 0};
+const camera_metadata_rational_t kControlAeCompensationStep[] = {{0, 1}};
+const uint8_t kAfTrigger = ANDROID_CONTROL_AF_TRIGGER_IDLE;
+const uint8_t kAfMode = ANDROID_CONTROL_AF_MODE_OFF;
+const uint8_t kAfAvailableModes[] = {ANDROID_CONTROL_AF_MODE_OFF};
+const uint8_t kAvailableSceneMode = ANDROID_CONTROL_SCENE_MODE_DISABLED;
+const uint8_t kAeLockAvailable = ANDROID_CONTROL_AE_LOCK_AVAILABLE_FALSE;
+const uint8_t kAwbLockAvailable = ANDROID_CONTROL_AWB_LOCK_AVAILABLE_FALSE;
+const int32_t kHalFormats[] = {HAL_PIXEL_FORMAT_BLOB,
+                               HAL_PIXEL_FORMAT_YCbCr_420_888,
+                               HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED};
+const int32_t kRequestMaxNumOutputStreams[] = {
+    /*RAW*/ 0,
+    /*Processed*/ 2,
+    /*Stall*/ 1};
+const uint8_t kAvailableCapabilities[] = {
+    ANDROID_REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE};
+const int32_t kAvailableRequestKeys[] = {
+    ANDROID_COLOR_CORRECTION_ABERRATION_MODE,
+    ANDROID_CONTROL_AE_ANTIBANDING_MODE,
+    ANDROID_CONTROL_AE_EXPOSURE_COMPENSATION,
+    ANDROID_CONTROL_AE_LOCK,
+    ANDROID_CONTROL_AE_MODE,
+    ANDROID_CONTROL_AE_PRECAPTURE_TRIGGER,
+    ANDROID_CONTROL_AE_TARGET_FPS_RANGE,
+    ANDROID_CONTROL_AF_MODE,
+    ANDROID_CONTROL_AF_TRIGGER,
+    ANDROID_CONTROL_AWB_LOCK,
+    ANDROID_CONTROL_AWB_MODE,
+    ANDROID_CONTROL_CAPTURE_INTENT,
+    ANDROID_CONTROL_EFFECT_MODE,
+    ANDROID_CONTROL_MODE,
+    ANDROID_CONTROL_SCENE_MODE,
+    ANDROID_CONTROL_VIDEO_STABILIZATION_MODE,
+    ANDROID_FLASH_MODE,
+    ANDROID_JPEG_ORIENTATION,
+    ANDROID_JPEG_QUALITY,
+    ANDROID_JPEG_THUMBNAIL_QUALITY,
+    ANDROID_JPEG_THUMBNAIL_SIZE,
+    ANDROID_LENS_OPTICAL_STABILIZATION_MODE,
+    ANDROID_NOISE_REDUCTION_MODE,
+    ANDROID_SCALER_CROP_REGION,
+    ANDROID_SENSOR_TEST_PATTERN_MODE,
+    ANDROID_STATISTICS_FACE_DETECT_MODE,
+    ANDROID_STATISTICS_HOT_PIXEL_MAP_MODE};
+const int32_t kAvailableResultKeys[] = {
+    ANDROID_COLOR_CORRECTION_ABERRATION_MODE,
+    ANDROID_CONTROL_AE_ANTIBANDING_MODE,
+    ANDROID_CONTROL_AE_EXPOSURE_COMPENSATION,
+    ANDROID_CONTROL_AE_LOCK,
+    ANDROID_CONTROL_AE_MODE,
+    ANDROID_CONTROL_AE_PRECAPTURE_TRIGGER,
+    ANDROID_CONTROL_AE_STATE,
+    ANDROID_CONTROL_AE_TARGET_FPS_RANGE,
+    ANDROID_CONTROL_AF_MODE,
+    ANDROID_CONTROL_AF_STATE,
+    ANDROID_CONTROL_AF_TRIGGER,
+    ANDROID_CONTROL_AWB_LOCK,
+    ANDROID_CONTROL_AWB_MODE,
+    ANDROID_CONTROL_AWB_STATE,
+    ANDROID_CONTROL_CAPTURE_INTENT,
+    ANDROID_CONTROL_EFFECT_MODE,
+    ANDROID_CONTROL_MODE,
+    ANDROID_CONTROL_SCENE_MODE,
+    ANDROID_CONTROL_VIDEO_STABILIZATION_MODE,
+    ANDROID_FLASH_MODE,
+    ANDROID_FLASH_STATE,
+    ANDROID_JPEG_ORIENTATION,
+    ANDROID_JPEG_QUALITY,
+    ANDROID_JPEG_THUMBNAIL_QUALITY,
+    ANDROID_JPEG_THUMBNAIL_SIZE,
+    ANDROID_LENS_OPTICAL_STABILIZATION_MODE,
+    ANDROID_NOISE_REDUCTION_MODE,
+    ANDROID_REQUEST_PIPELINE_DEPTH,
+    ANDROID_SCALER_CROP_REGION,
+    ANDROID_SENSOR_TIMESTAMP,
+    ANDROID_STATISTICS_FACE_DETECT_MODE,
+    ANDROID_STATISTICS_HOT_PIXEL_MAP_MODE,
+    ANDROID_STATISTICS_LENS_SHADING_MAP_MODE,
+    ANDROID_STATISTICS_SCENE_FLICKER};
+const int32_t kAvailableCharacteristicsKeys[] = {
+    ANDROID_COLOR_CORRECTION_AVAILABLE_ABERRATION_MODES,
+    ANDROID_CONTROL_AE_AVAILABLE_ANTIBANDING_MODES,
+    ANDROID_CONTROL_AE_AVAILABLE_MODES,
+    ANDROID_CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES,
+    ANDROID_CONTROL_AE_COMPENSATION_RANGE,
+    ANDROID_CONTROL_AE_COMPENSATION_STEP,
+    ANDROID_CONTROL_AE_LOCK_AVAILABLE,
+    ANDROID_CONTROL_AF_AVAILABLE_MODES,
+    ANDROID_CONTROL_AVAILABLE_EFFECTS,
+    ANDROID_CONTROL_AVAILABLE_MODES,
+    ANDROID_CONTROL_AVAILABLE_SCENE_MODES,
+    ANDROID_CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES,
+    ANDROID_CONTROL_AWB_AVAILABLE_MODES,
+    ANDROID_CONTROL_AWB_LOCK_AVAILABLE,
+    ANDROID_CONTROL_MAX_REGIONS,
+    ANDROID_FLASH_INFO_AVAILABLE,
+    ANDROID_INFO_SUPPORTED_HARDWARE_LEVEL,
+    ANDROID_JPEG_AVAILABLE_THUMBNAIL_SIZES,
+    ANDROID_LENS_FACING,
+    ANDROID_LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION,
+    ANDROID_LENS_INFO_FOCUS_DISTANCE_CALIBRATION,
+    ANDROID_LENS_INFO_MINIMUM_FOCUS_DISTANCE,
+    ANDROID_NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES,
+    ANDROID_REQUEST_AVAILABLE_CAPABILITIES,
+    ANDROID_REQUEST_MAX_NUM_INPUT_STREAMS,
+    ANDROID_REQUEST_MAX_NUM_OUTPUT_STREAMS,
+    ANDROID_REQUEST_PARTIAL_RESULT_COUNT,
+    ANDROID_REQUEST_PIPELINE_MAX_DEPTH,
+    ANDROID_SCALER_AVAILABLE_MAX_DIGITAL_ZOOM,
+    ANDROID_SCALER_AVAILABLE_STREAM_CONFIGURATIONS,
+    ANDROID_SCALER_CROPPING_TYPE,
+    ANDROID_SENSOR_INFO_ACTIVE_ARRAY_SIZE,
+    ANDROID_SENSOR_INFO_MAX_FRAME_DURATION,
+    ANDROID_SENSOR_INFO_PIXEL_ARRAY_SIZE,
+    ANDROID_SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE,
+    ANDROID_SENSOR_INFO_TIMESTAMP_SOURCE,
+    ANDROID_SENSOR_ORIENTATION,
+    ANDROID_SHADING_AVAILABLE_MODES,
+    ANDROID_STATISTICS_INFO_AVAILABLE_FACE_DETECT_MODES,
+    ANDROID_STATISTICS_INFO_AVAILABLE_HOT_PIXEL_MAP_MODES,
+    ANDROID_STATISTICS_INFO_AVAILABLE_LENS_SHADING_MAP_MODES,
+    ANDROID_STATISTICS_INFO_MAX_FACE_COUNT,
+    ANDROID_SYNC_MAX_LATENCY};
+const std::map<RequestTemplate, uint8_t> kTemplateToIntent = {
+    {RequestTemplate::PREVIEW, ANDROID_CONTROL_CAPTURE_INTENT_PREVIEW},
+    {RequestTemplate::STILL_CAPTURE,
+     ANDROID_CONTROL_CAPTURE_INTENT_STILL_CAPTURE},
+    {RequestTemplate::VIDEO_RECORD,
+     ANDROID_CONTROL_CAPTURE_INTENT_VIDEO_RECORD},
+    {RequestTemplate::VIDEO_SNAPSHOT,
+     ANDROID_CONTROL_CAPTURE_INTENT_VIDEO_SNAPSHOT},
+};
+}  // namespace
+
+// Constructor sets the default characteristics for vsock camera
+VsockCameraMetadata::VsockCameraMetadata(int32_t width, int32_t height,
+                                         int32_t fps)
+    : width_(width), height_(height), fps_(fps) {
+  update(ANDROID_CONTROL_AE_COMPENSATION_RANGE, kControlAeCompensationRange,
+         NELEM(kControlAeCompensationRange));
+  update(ANDROID_CONTROL_AE_COMPENSATION_STEP, kControlAeCompensationStep,
+         NELEM(kControlAeCompensationStep));
+  update(ANDROID_CONTROL_AF_AVAILABLE_MODES, kAfAvailableModes,
+         NELEM(kAfAvailableModes));
+  update(ANDROID_CONTROL_AVAILABLE_SCENE_MODES, &kAvailableSceneMode, 1);
+  update(ANDROID_CONTROL_AE_LOCK_AVAILABLE, &kAeLockAvailable, 1);
+  update(ANDROID_CONTROL_AWB_LOCK_AVAILABLE, &kAwbLockAvailable, 1);
+  update(ANDROID_SCALER_AVAILABLE_MAX_DIGITAL_ZOOM,
+         kScalerAvailableMaxDigitalZoom, NELEM(kScalerAvailableMaxDigitalZoom));
+  update(ANDROID_REQUEST_AVAILABLE_CAPABILITIES, kAvailableCapabilities,
+         NELEM(kAvailableCapabilities));
+  update(ANDROID_INFO_SUPPORTED_HARDWARE_LEVEL, &kHardwarelevel, 1);
+  update(ANDROID_COLOR_CORRECTION_AVAILABLE_ABERRATION_MODES,
+         kAvailableAberrationModes, NELEM(kAvailableAberrationModes));
+  update(ANDROID_CONTROL_AE_AVAILABLE_ANTIBANDING_MODES, &kAntibandingMode, 1);
+  update(ANDROID_CONTROL_MAX_REGIONS, kControlMaxRegions,
+         NELEM(kControlMaxRegions));
+  update(ANDROID_CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES,
+         &kVideoStabilizationMode, 1);
+  update(ANDROID_CONTROL_AWB_AVAILABLE_MODES, &kAwbAvailableMode, 1);
+  update(ANDROID_CONTROL_AE_AVAILABLE_MODES, &kAeAvailableMode, 1);
+  update(ANDROID_CONTROL_AVAILABLE_EFFECTS, &kAvailableFffect, 1);
+  update(ANDROID_CONTROL_AVAILABLE_MODES, kControlAvailableModes,
+         NELEM(kControlAvailableModes));
+  update(ANDROID_EDGE_AVAILABLE_EDGE_MODES, &kEdgeMode, 1);
+  update(ANDROID_FLASH_INFO_AVAILABLE, &kFlashInfo, 1);
+  update(ANDROID_HOT_PIXEL_AVAILABLE_HOT_PIXEL_MODES, &kHotPixelMode, 1);
+  update(ANDROID_JPEG_AVAILABLE_THUMBNAIL_SIZES, kJpegAvailableThumbnailSizes,
+         NELEM(kJpegAvailableThumbnailSizes));
+  update(ANDROID_LENS_INFO_FOCUS_DISTANCE_CALIBRATION,
+         &kFocusDistanceCalibration, 1);
+  update(ANDROID_LENS_INFO_MINIMUM_FOCUS_DISTANCE, &kLensMinFocusDistance, 1);
+  update(ANDROID_LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION,
+         &kOpticalStabilizationMode, 1);
+  update(ANDROID_LENS_FACING, &kFacing, 1);
+  update(ANDROID_NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES,
+         &kNoiseReductionMode, 1);
+  update(ANDROID_NOISE_REDUCTION_MODE, &kNoiseReductionMode, 1);
+  update(ANDROID_REQUEST_PARTIAL_RESULT_COUNT, &kPartialResultCount, 1);
+  update(ANDROID_REQUEST_PIPELINE_MAX_DEPTH, &kRequestPipelineMaxDepth, 1);
+  update(ANDROID_REQUEST_MAX_NUM_OUTPUT_STREAMS, kRequestMaxNumOutputStreams,
+         NELEM(kRequestMaxNumOutputStreams));
+  update(ANDROID_REQUEST_MAX_NUM_INPUT_STREAMS, &kRequestMaxNumInputStreams, 1);
+  update(ANDROID_SCALER_AVAILABLE_MAX_DIGITAL_ZOOM,
+         kScalerAvailableMaxDigitalZoom, NELEM(kScalerAvailableMaxDigitalZoom));
+  update(ANDROID_SCALER_CROPPING_TYPE, &kCroppingType, 1);
+  update(ANDROID_SENSOR_AVAILABLE_TEST_PATTERN_MODES, kTestPatternModes,
+         NELEM(kTestPatternModes));
+  update(ANDROID_SENSOR_INFO_TIMESTAMP_SOURCE, &kTimestampSource, 1);
+  update(ANDROID_SENSOR_ORIENTATION, &kOrientation, 1);
+  update(ANDROID_SHADING_AVAILABLE_MODES, &kAvailableShadingMode, 1);
+  update(ANDROID_STATISTICS_INFO_AVAILABLE_FACE_DETECT_MODES, &kFaceDetectMode,
+         1);
+  update(ANDROID_STATISTICS_INFO_MAX_FACE_COUNT, &kMaxFaceCount, 1);
+  update(ANDROID_STATISTICS_INFO_AVAILABLE_HOT_PIXEL_MAP_MODES,
+         &kAvailableHotpixelMode, 1);
+  update(ANDROID_STATISTICS_INFO_AVAILABLE_LENS_SHADING_MAP_MODES,
+         &kLensShadingMapMode, 1);
+  update(ANDROID_SYNC_MAX_LATENCY, &kMaxLatency, 1);
+  update(ANDROID_REQUEST_AVAILABLE_REQUEST_KEYS, kAvailableRequestKeys,
+         NELEM(kAvailableRequestKeys));
+  update(ANDROID_REQUEST_AVAILABLE_RESULT_KEYS, kAvailableResultKeys,
+         NELEM(kAvailableResultKeys));
+  update(ANDROID_REQUEST_AVAILABLE_CHARACTERISTICS_KEYS,
+         kAvailableCharacteristicsKeys, NELEM(kAvailableCharacteristicsKeys));
+
+  // assume max 2bytes/pixel + info because client might provide us pngs
+  const int32_t jpeg_max_size = width * height * 2 + sizeof(camera3_jpeg_blob);
+  update(ANDROID_JPEG_MAX_SIZE, &jpeg_max_size, 1);
+
+  std::vector<int64_t> min_frame_durations;
+  std::vector<int32_t> stream_configurations;
+  std::vector<int64_t> stall_durations;
+
+  int64_t frame_duration = 1000000000L / fps;
+  for (const auto& format : kHalFormats) {
+    stream_configurations.push_back(format);
+    min_frame_durations.push_back(format);
+    stall_durations.push_back(format);
+    stream_configurations.push_back(width);
+    min_frame_durations.push_back(width);
+    stall_durations.push_back(width);
+    stream_configurations.push_back(height);
+    min_frame_durations.push_back(height);
+    stall_durations.push_back(height);
+    stream_configurations.push_back(
+        ANDROID_SCALER_AVAILABLE_STREAM_CONFIGURATIONS_OUTPUT);
+    min_frame_durations.push_back(frame_duration);
+    stall_durations.push_back((format == HAL_PIXEL_FORMAT_BLOB) ? 2000000000L
+                                                                : 0);
+  }
+  update(ANDROID_SCALER_AVAILABLE_STREAM_CONFIGURATIONS,
+         stream_configurations.data(), stream_configurations.size());
+  update(ANDROID_SCALER_AVAILABLE_MIN_FRAME_DURATIONS,
+         min_frame_durations.data(), min_frame_durations.size());
+  update(ANDROID_SCALER_AVAILABLE_STALL_DURATIONS, stall_durations.data(),
+         stall_durations.size());
+
+  int32_t active_array_size[] = {0, 0, width, height};
+  update(ANDROID_SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE,
+         active_array_size, NELEM(active_array_size));
+  update(ANDROID_SENSOR_INFO_ACTIVE_ARRAY_SIZE, active_array_size,
+         NELEM(active_array_size));
+
+  int32_t pixel_array_size[] = {width, height};
+  update(ANDROID_SENSOR_INFO_PIXEL_ARRAY_SIZE, pixel_array_size,
+         NELEM(pixel_array_size));
+
+  int32_t max_frame_rate = fps;
+  int32_t min_frame_rate = max_frame_rate / 2;
+  int32_t frame_rates[] = {min_frame_rate, max_frame_rate};
+  update(ANDROID_CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES, frame_rates,
+         NELEM(frame_rates));
+  int64_t max_frame_duration = 1000000000L / min_frame_rate;
+  update(ANDROID_SENSOR_INFO_MAX_FRAME_DURATION, &max_frame_duration, 1);
+}
+
+VsockCameraRequestMetadata::VsockCameraRequestMetadata(int32_t fps,
+                                                       RequestTemplate type) {
+  update(ANDROID_COLOR_CORRECTION_ABERRATION_MODE, &kAberrationMode, 1);
+  update(ANDROID_CONTROL_AE_EXPOSURE_COMPENSATION, &kExposureCompensation, 1);
+  update(ANDROID_CONTROL_VIDEO_STABILIZATION_MODE, &kVideoStabilizationMode, 1);
+  update(ANDROID_CONTROL_AWB_MODE, &kAwbAvailableMode, 1);
+  update(ANDROID_CONTROL_AE_MODE, &kAeAvailableMode, 1);
+  update(ANDROID_CONTROL_AE_PRECAPTURE_TRIGGER, &kAePrecaptureTrigger, 1);
+  update(ANDROID_CONTROL_AF_MODE, &kAfMode, 1);
+  update(ANDROID_CONTROL_AF_TRIGGER, &kAfTrigger, 1);
+  update(ANDROID_CONTROL_SCENE_MODE, &kAvailableSceneMode, 1);
+  update(ANDROID_CONTROL_EFFECT_MODE, &kAvailableFffect, 1);
+  update(ANDROID_FLASH_MODE, &kFlashMode, 1);
+  update(ANDROID_JPEG_THUMBNAIL_SIZE, kThumbnailSize, NELEM(kThumbnailSize));
+  update(ANDROID_JPEG_QUALITY, &kJpegQuality, 1);
+  update(ANDROID_JPEG_THUMBNAIL_QUALITY, &kJpegQuality, 1);
+  update(ANDROID_JPEG_ORIENTATION, &kJpegOrientation, 1);
+  update(ANDROID_LENS_OPTICAL_STABILIZATION_MODE, &kOpticalStabilizationMode,
+         1);
+  update(ANDROID_NOISE_REDUCTION_MODE, &kNoiseReductionMode, 1);
+  update(ANDROID_SENSOR_TEST_PATTERN_MODE, &kTestPatternMode, 1);
+  update(ANDROID_STATISTICS_FACE_DETECT_MODE, &kFaceDetectMode, 1);
+  update(ANDROID_STATISTICS_HOT_PIXEL_MAP_MODE, &kAvailableHotpixelMode, 1);
+
+  int32_t max_frame_rate = fps;
+  int32_t min_frame_rate = max_frame_rate / 2;
+  int32_t frame_rates[] = {min_frame_rate, max_frame_rate};
+  update(ANDROID_CONTROL_AE_TARGET_FPS_RANGE, frame_rates, NELEM(frame_rates));
+
+  update(ANDROID_CONTROL_AE_ANTIBANDING_MODE, &kAntibandingMode, 1);
+  update(ANDROID_CONTROL_MODE, &kControlMode, 1);
+
+  auto it = kTemplateToIntent.find(type);
+  if (it != kTemplateToIntent.end()) {
+    auto intent = it->second;
+    update(ANDROID_CONTROL_CAPTURE_INTENT, &intent, 1);
+    is_valid_ = true;
+  } else {
+    is_valid_ = false;
+  }
+}
+
+}  // namespace android::hardware::camera::device::V3_4::implementation
diff --git a/guest/hals/camera/vsock_camera_metadata.h b/guest/hals/camera/vsock_camera_metadata.h
new file mode 100644
index 0000000..d5b43c7
--- /dev/null
+++ b/guest/hals/camera/vsock_camera_metadata.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+#pragma once
+
+#include <CameraMetadata.h>
+#include <android/hardware/camera/device/3.2/ICameraDevice.h>
+
+namespace android::hardware::camera::device::V3_4::implementation {
+
+// Small wrappers for mostly hard-coded camera metadata
+// Some parameters are calculated from remote camera frame size and fps
+using ::android::hardware::camera::common::V1_0::helper::CameraMetadata;
+class VsockCameraMetadata : public CameraMetadata {
+ public:
+  VsockCameraMetadata(int32_t width, int32_t height, int32_t fps);
+
+  int32_t getPreferredWidth() const { return width_; }
+  int32_t getPreferredHeight() const { return height_; }
+  int32_t getPreferredFps() const { return fps_; }
+
+ private:
+  int32_t width_;
+  int32_t height_;
+  int32_t fps_;
+};
+
+using ::android::hardware::camera::device::V3_2::RequestTemplate;
+class VsockCameraRequestMetadata : public CameraMetadata {
+ public:
+  VsockCameraRequestMetadata(int32_t fps, RequestTemplate type);
+  // Tells whether the metadata has been successfully constructed
+  // from the parameters
+  bool isValid() const { return is_valid_; }
+
+ private:
+  bool is_valid_;
+};
+
+}  // namespace android::hardware::camera::device::V3_4::implementation
diff --git a/guest/hals/camera/vsock_camera_provider_2_7.cpp b/guest/hals/camera/vsock_camera_provider_2_7.cpp
new file mode 100644
index 0000000..dbe0092
--- /dev/null
+++ b/guest/hals/camera/vsock_camera_provider_2_7.cpp
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2021 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.
+ */
+#define LOG_TAG "VsockCameraProvider"
+#include "vsock_camera_provider_2_7.h"
+#include <cutils/properties.h>
+#include <log/log.h>
+#include "vsock_camera_server.h"
+
+namespace android::hardware::camera::provider::V2_7::implementation {
+
+namespace {
+VsockCameraServer gCameraServer;
+constexpr auto kDeviceName = "[email protected]/external/0";
+}  // namespace
+
+using android::hardware::camera::provider::V2_7::ICameraProvider;
+extern "C" ICameraProvider* HIDL_FETCH_ICameraProvider(const char* name) {
+  return (strcmp(name, "external/0") == 0)
+             ? new VsockCameraProvider(&gCameraServer)
+             : nullptr;
+}
+
+VsockCameraProvider::VsockCameraProvider(VsockCameraServer* server) {
+  server_ = server;
+  if (!server->isRunning()) {
+    constexpr static const auto camera_port_property =
+        "ro.boot.vsock_camera_port";
+    constexpr static const auto camera_cid_property =
+        "ro.boot.vsock_camera_cid";
+    auto port = property_get_int32(camera_port_property, -1);
+    auto cid = property_get_int32(camera_cid_property, -1);
+    if (port > 0) {
+      server->start(port, cid);
+    }
+  }
+}
+
+VsockCameraProvider::~VsockCameraProvider() {
+  server_->setConnectedCallback(nullptr);
+}
+
+Return<Status> VsockCameraProvider::setCallback(
+    const sp<ICameraProviderCallback>& callback) {
+  {
+    std::lock_guard<std::mutex> lock(mutex_);
+    callbacks_ = callback;
+  }
+  server_->setConnectedCallback(
+      [this](std::shared_ptr<cuttlefish::VsockConnection> connection,
+             VsockCameraDevice::Settings settings) {
+        connection_ = connection;
+        settings_ = settings;
+        deviceAdded(kDeviceName);
+        connection_->SetDisconnectCallback(
+            [this] { deviceRemoved(kDeviceName); });
+      });
+  return Status::OK;
+}
+
+Return<void> VsockCameraProvider::getVendorTags(
+    ICameraProvider::getVendorTags_cb _hidl_cb) {
+  // No vendor tag support
+  hidl_vec<VendorTagSection> empty;
+  _hidl_cb(Status::OK, empty);
+  return Void();
+}
+
+Return<void> VsockCameraProvider::getCameraIdList(
+    ICameraProvider::getCameraIdList_cb _hidl_cb) {
+  // External camera HAL always report 0 camera, and extra cameras
+  // are just reported via cameraDeviceStatusChange callbacks
+  hidl_vec<hidl_string> empty;
+  _hidl_cb(Status::OK, empty);
+  return Void();
+}
+
+Return<void> VsockCameraProvider::isSetTorchModeSupported(
+    ICameraProvider::isSetTorchModeSupported_cb _hidl_cb) {
+  // setTorchMode API is supported, though right now no external camera device
+  // has a flash unit.
+  _hidl_cb(Status::OK, true);
+  return Void();
+}
+
+Return<void> VsockCameraProvider::getCameraDeviceInterface_V1_x(
+    const hidl_string&,
+    ICameraProvider::getCameraDeviceInterface_V1_x_cb _hidl_cb) {
+  // External Camera HAL does not support HAL1
+  _hidl_cb(Status::OPERATION_NOT_SUPPORTED, nullptr);
+  return Void();
+}
+
+Return<void> VsockCameraProvider::getCameraDeviceInterface_V3_x(
+    const hidl_string& name_hidl_str,
+    ICameraProvider::getCameraDeviceInterface_V3_x_cb _hidl_cb) {
+  std::string name(name_hidl_str.c_str());
+  if (name != kDeviceName) {
+    _hidl_cb(Status::ILLEGAL_ARGUMENT, nullptr);
+    return Void();
+  }
+
+  _hidl_cb(Status::OK, new VsockCameraDevice(name, settings_, connection_));
+  return Void();
+}
+
+Return<void> VsockCameraProvider::notifyDeviceStateChange(
+    hardware::hidl_bitfield<DeviceState> /*newState*/) {
+  return Void();
+}
+
+Return<void> VsockCameraProvider::getConcurrentStreamingCameraIds(
+    getConcurrentStreamingCameraIds_cb _hidl_cb) {
+  hidl_vec<hidl_vec<hidl_string>> hidl_camera_id_combinations;
+  _hidl_cb(Status::OK, hidl_camera_id_combinations);
+  return Void();
+}
+
+Return<void> VsockCameraProvider::isConcurrentStreamCombinationSupported(
+    const hidl_vec<::android::hardware::camera::provider::V2_6::CameraIdAndStreamCombination>&,
+    isConcurrentStreamCombinationSupported_cb _hidl_cb) {
+  _hidl_cb(Status::OK, false);
+  return Void();
+}
+
+Return<void> VsockCameraProvider::isConcurrentStreamCombinationSupported_2_7(
+    const hidl_vec<::android::hardware::camera::provider::V2_7::CameraIdAndStreamCombination>&,
+    isConcurrentStreamCombinationSupported_2_7_cb _hidl_cb) {
+  _hidl_cb(Status::OK, false);
+  return Void();
+}
+
+void VsockCameraProvider::deviceAdded(const char* name) {
+  std::lock_guard<std::mutex> lock(mutex_);
+  if (callbacks_ != nullptr) {
+    callbacks_->cameraDeviceStatusChange(name, CameraDeviceStatus::PRESENT);
+  }
+}
+
+void VsockCameraProvider::deviceRemoved(const char* name) {
+  std::lock_guard<std::mutex> lock(mutex_);
+  if (callbacks_ != nullptr) {
+    callbacks_->cameraDeviceStatusChange(name, CameraDeviceStatus::NOT_PRESENT);
+  }
+}
+
+}  // namespace android::hardware::camera::provider::V2_7::implementation
diff --git a/guest/hals/camera/vsock_camera_provider_2_7.h b/guest/hals/camera/vsock_camera_provider_2_7.h
new file mode 100644
index 0000000..7ab9997
--- /dev/null
+++ b/guest/hals/camera/vsock_camera_provider_2_7.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+#pragma once
+
+#include <mutex>
+
+#include <android/hardware/camera/provider/2.7/ICameraProvider.h>
+#include <hidl/MQDescriptor.h>
+#include <hidl/Status.h>
+#include <json/json.h>
+
+#include "vsock_camera_device_3_4.h"
+#include "vsock_camera_server.h"
+#include "vsock_connection.h"
+
+namespace android::hardware::camera::provider::V2_7::implementation {
+
+using ::android::sp;
+using ::android::hardware::hidl_string;
+using ::android::hardware::Return;
+using ::android::hardware::camera::common::V1_0::CameraDeviceStatus;
+using ::android::hardware::camera::common::V1_0::Status;
+using ::android::hardware::camera::common::V1_0::VendorTagSection;
+using ::android::hardware::camera::device::V3_4::implementation::
+    VsockCameraDevice;
+using ::android::hardware::camera::provider::V2_4::ICameraProviderCallback;
+using ::android::hardware::camera::provider::V2_5::DeviceState;
+using ::android::hardware::camera::provider::V2_7::ICameraProvider;
+
+class VsockCameraProvider : public ICameraProvider {
+ public:
+  VsockCameraProvider(VsockCameraServer* server);
+  ~VsockCameraProvider();
+
+  Return<Status> setCallback(
+      const sp<ICameraProviderCallback>& callback) override;
+  Return<void> getVendorTags(getVendorTags_cb _hidl_cb) override;
+  Return<void> getCameraIdList(getCameraIdList_cb _hidl_cb) override;
+  Return<void> isSetTorchModeSupported(
+      isSetTorchModeSupported_cb _hidl_cb) override;
+  Return<void> getCameraDeviceInterface_V1_x(
+      const hidl_string& cameraDeviceName,
+      getCameraDeviceInterface_V1_x_cb _hidl_cb) override;
+  Return<void> getCameraDeviceInterface_V3_x(
+      const hidl_string& cameraDeviceName,
+      getCameraDeviceInterface_V3_x_cb _hidl_cb) override;
+  Return<void> notifyDeviceStateChange(
+      hardware::hidl_bitfield<DeviceState> newState) override;
+  Return<void> getConcurrentStreamingCameraIds(
+      getConcurrentStreamingCameraIds_cb _hidl_cb) override;
+  Return<void> isConcurrentStreamCombinationSupported(
+      const hidl_vec<::android::hardware::camera::provider::V2_6::CameraIdAndStreamCombination>& configs,
+      isConcurrentStreamCombinationSupported_cb _hidl_cb) override;
+  Return<void> isConcurrentStreamCombinationSupported_2_7(
+      const hidl_vec<::android::hardware::camera::provider::V2_7::CameraIdAndStreamCombination>& configs,
+      isConcurrentStreamCombinationSupported_2_7_cb _hidl_cb) override;
+
+ private:
+  void deviceRemoved(const char* name);
+  void deviceAdded(const char* name);
+  std::mutex mutex_;
+  sp<ICameraProviderCallback> callbacks_;
+  std::shared_ptr<cuttlefish::VsockConnection> connection_;
+  VsockCameraDevice::Settings settings_;
+  VsockCameraServer* server_;
+};
+
+}  // namespace android::hardware::camera::provider::V2_7::implementation
diff --git a/guest/hals/camera/vsock_camera_server.cpp b/guest/hals/camera/vsock_camera_server.cpp
new file mode 100644
index 0000000..ddb574c
--- /dev/null
+++ b/guest/hals/camera/vsock_camera_server.cpp
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+#define LOG_TAG "VsockCameraServer"
+#include "vsock_camera_server.h"
+#include <log/log.h>
+
+namespace android::hardware::camera::provider::V2_7::implementation {
+
+using ::android::hardware::camera::device::V3_4::implementation::
+    VsockCameraDevice;
+namespace {
+
+bool containsValidSettings(const VsockCameraDevice::Settings& settings) {
+  return settings.width > 0 && settings.height > 0 && settings.frame_rate > 0.0;
+}
+
+bool readSettingsFromJson(VsockCameraDevice::Settings& settings,
+                          const Json::Value& json) {
+  VsockCameraDevice::Settings new_settings;
+  new_settings.width = json["width"].asInt();
+  new_settings.height = json["height"].asInt();
+  new_settings.frame_rate = json["frame_rate"].asDouble();
+  if (containsValidSettings(new_settings)) {
+    settings = new_settings;
+    return true;
+  } else {
+    return false;
+  }
+}
+
+}  // namespace
+
+VsockCameraServer::VsockCameraServer() {
+  ALOGI("%s: Create server", __FUNCTION__);
+  connection_ = std::make_shared<cuttlefish::VsockServerConnection>();
+}
+
+VsockCameraServer::~VsockCameraServer() {
+  ALOGI("%s: Destroy server", __FUNCTION__);
+  stop();
+}
+
+void VsockCameraServer::start(unsigned int port, unsigned int cid) {
+  stop();
+  is_running_ = true;
+  server_thread_ = std::thread([this, port, cid] { serverLoop(port, cid); });
+}
+
+void VsockCameraServer::stop() {
+  connection_->ServerShutdown();
+  is_running_ = false;
+  if (server_thread_.joinable()) {
+    server_thread_.join();
+  }
+}
+
+void VsockCameraServer::setConnectedCallback(callback_t callback) {
+  connected_callback_ = callback;
+  std::lock_guard<std::mutex> lock(settings_mutex_);
+  if (callback && connection_->IsConnected() &&
+      containsValidSettings(settings_)) {
+    callback(connection_, settings_);
+  }
+}
+
+void VsockCameraServer::serverLoop(unsigned int port, unsigned int cid) {
+  while (is_running_.load()) {
+    ALOGI("%s: Accepting connections...", __FUNCTION__);
+    if (connection_->Connect(port, cid)) {
+      auto json_settings = connection_->ReadJsonMessage();
+      VsockCameraDevice::Settings settings;
+      if (readSettingsFromJson(settings, json_settings)) {
+        std::lock_guard<std::mutex> lock(settings_mutex_);
+        settings_ = settings;
+        if (connected_callback_) {
+          connected_callback_(connection_, settings);
+        }
+        ALOGI("%s: Client connected", __FUNCTION__);
+      } else {
+        ALOGE("%s: Could not read settings", __FUNCTION__);
+      }
+    } else {
+      ALOGE("%s: Accepting connections failed", __FUNCTION__);
+    }
+  }
+  ALOGI("%s: Exiting", __FUNCTION__);
+}
+
+}  // namespace android::hardware::camera::provider::V2_7::implementation
diff --git a/guest/hals/camera/vsock_camera_server.h b/guest/hals/camera/vsock_camera_server.h
new file mode 100644
index 0000000..f807dcf
--- /dev/null
+++ b/guest/hals/camera/vsock_camera_server.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+#pragma once
+#include <atomic>
+#include <thread>
+#include "vsock_camera_device_3_4.h"
+#include "vsock_connection.h"
+
+namespace android::hardware::camera::provider::V2_7::implementation {
+
+using ::android::hardware::camera::device::V3_4::implementation::
+    VsockCameraDevice;
+
+class VsockCameraServer {
+ public:
+  VsockCameraServer();
+  ~VsockCameraServer();
+
+  VsockCameraServer(const VsockCameraServer&) = delete;
+  VsockCameraServer& operator=(const VsockCameraServer&) = delete;
+
+  bool isRunning() const { return is_running_.load(); }
+
+  void start(unsigned int port, unsigned int cid);
+  void stop();
+
+  using callback_t =
+      std::function<void(std::shared_ptr<cuttlefish::VsockConnection>,
+                         VsockCameraDevice::Settings)>;
+  void setConnectedCallback(callback_t callback);
+
+ private:
+  void serverLoop(unsigned int port, unsigned int cid);
+  std::thread server_thread_;
+  std::atomic<bool> is_running_;
+  std::shared_ptr<cuttlefish::VsockServerConnection> connection_;
+  std::mutex settings_mutex_;
+  VsockCameraDevice::Settings settings_;
+  callback_t connected_callback_;
+};
+
+}  // namespace android::hardware::camera::provider::V2_7::implementation
diff --git a/guest/hals/camera/vsock_frame_provider.cpp b/guest/hals/camera/vsock_frame_provider.cpp
new file mode 100644
index 0000000..435d5f9
--- /dev/null
+++ b/guest/hals/camera/vsock_frame_provider.cpp
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+#include "vsock_frame_provider.h"
+#include <hardware/camera3.h>
+#include <libyuv.h>
+#include <cstring>
+#define LOG_TAG "VsockFrameProvider"
+#include <log/log.h>
+
+namespace cuttlefish {
+
+VsockFrameProvider::~VsockFrameProvider() { stop(); }
+
+void VsockFrameProvider::start(
+    std::shared_ptr<cuttlefish::VsockConnection> connection, uint32_t width,
+    uint32_t height) {
+  stop();
+  running_ = true;
+  connection_ = connection;
+  reader_thread_ =
+      std::thread([this, width, height] { VsockReadLoop(width, height); });
+}
+
+void VsockFrameProvider::stop() {
+  running_ = false;
+  jpeg_pending_ = false;
+  if (reader_thread_.joinable()) {
+    reader_thread_.join();
+  }
+  connection_ = nullptr;
+}
+
+bool VsockFrameProvider::waitYUVFrame(unsigned int max_wait_ms) {
+  auto timeout = std::chrono::milliseconds(max_wait_ms);
+  nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
+  std::unique_lock<std::mutex> lock(frame_mutex_);
+  return yuv_frame_updated_.wait_for(
+      lock, timeout, [this, now] { return timestamp_.load() > now; });
+}
+
+void VsockFrameProvider::requestJpeg() {
+  jpeg_pending_ = true;
+  Json::Value message;
+  message["event"] = "VIRTUAL_DEVICE_CAPTURE_IMAGE";
+  if (connection_) {
+    connection_->WriteMessage(message);
+  }
+}
+
+void VsockFrameProvider::cancelJpegRequest() { jpeg_pending_ = false; }
+
+bool VsockFrameProvider::copyYUVFrame(uint32_t w, uint32_t h, YCbCrLayout dst) {
+  size_t y_size = w * h;
+  size_t cbcr_size = (w / 2) * (h / 2);
+  size_t total_size = y_size + 2 * cbcr_size;
+  std::lock_guard<std::mutex> lock(frame_mutex_);
+  if (frame_.size() < total_size) {
+    ALOGE("%s: %zu is too little for %ux%u frame", __FUNCTION__, frame_.size(),
+          w, h);
+    return false;
+  }
+  if (dst.y == nullptr) {
+    ALOGE("%s: Destination is nullptr!", __FUNCTION__);
+    return false;
+  }
+  YCbCrLayout src{.y = static_cast<void*>(frame_.data()),
+                  .cb = static_cast<void*>(frame_.data() + y_size),
+                  .cr = static_cast<void*>(frame_.data() + y_size + cbcr_size),
+                  .yStride = w,
+                  .cStride = w / 2,
+                  .chromaStep = 1};
+  uint8_t* src_y = static_cast<uint8_t*>(src.y);
+  uint8_t* dst_y = static_cast<uint8_t*>(dst.y);
+  uint8_t* src_cb = static_cast<uint8_t*>(src.cb);
+  uint8_t* dst_cb = static_cast<uint8_t*>(dst.cb);
+  uint8_t* src_cr = static_cast<uint8_t*>(src.cr);
+  uint8_t* dst_cr = static_cast<uint8_t*>(dst.cr);
+  libyuv::CopyPlane(src_y, src.yStride, dst_y, dst.yStride, w, h);
+  if (dst.chromaStep == 1) {
+    // Planar
+    libyuv::CopyPlane(src_cb, src.cStride, dst_cb, dst.cStride, w / 2, h / 2);
+    libyuv::CopyPlane(src_cr, src.cStride, dst_cr, dst.cStride, w / 2, h / 2);
+  } else if (dst.chromaStep == 2 && dst_cr - dst_cb == 1) {
+    // Interleaved cb/cr planes starting with cb
+    libyuv::MergeUVPlane(src_cb, src.cStride, src_cr, src.cStride, dst_cb,
+                         dst.cStride, w / 2, h / 2);
+  } else if (dst.chromaStep == 2 && dst_cb - dst_cr == 1) {
+    // Interleaved cb/cr planes starting with cr
+    libyuv::MergeUVPlane(src_cr, src.cStride, src_cb, src.cStride, dst_cr,
+                         dst.cStride, w / 2, h / 2);
+  } else {
+    ALOGE("%s: Unsupported interleaved U/V layout", __FUNCTION__);
+    return false;
+  }
+  return true;
+}
+
+bool VsockFrameProvider::copyJpegData(uint32_t size, void* dst) {
+  std::lock_guard<std::mutex> lock(jpeg_mutex_);
+  auto jpeg_header_offset = size - sizeof(struct camera3_jpeg_blob);
+  if (cached_jpeg_.empty()) {
+    ALOGE("%s: No source data", __FUNCTION__);
+    return false;
+  } else if (dst == nullptr) {
+    ALOGE("%s: Destination is nullptr", __FUNCTION__);
+    return false;
+  } else if (jpeg_header_offset <= cached_jpeg_.size()) {
+    ALOGE("%s: %ubyte target buffer too small", __FUNCTION__, size);
+    return false;
+  }
+  std::memcpy(dst, cached_jpeg_.data(), cached_jpeg_.size());
+  struct camera3_jpeg_blob* blob = reinterpret_cast<struct camera3_jpeg_blob*>(
+      static_cast<char*>(dst) + jpeg_header_offset);
+  blob->jpeg_blob_id = CAMERA3_JPEG_BLOB_ID;
+  blob->jpeg_size = cached_jpeg_.size();
+  cached_jpeg_.clear();
+  return true;
+}
+
+bool VsockFrameProvider::isBlob(const std::vector<char>& blob) {
+  bool is_png = blob.size() > 4 && (blob[0] & 0xff) == 0x89 &&
+                (blob[1] & 0xff) == 0x50 && (blob[2] & 0xff) == 0x4e &&
+                (blob[3] & 0xff) == 0x47;
+  bool is_jpeg =
+      blob.size() > 2 && (blob[0] & 0xff) == 0xff && (blob[1] & 0xff) == 0xd8;
+  return is_png || is_jpeg;
+}
+
+bool VsockFrameProvider::framesizeMatches(uint32_t width, uint32_t height,
+                                          const std::vector<char>& data) {
+  return data.size() == 3 * width * height / 2;
+}
+
+void VsockFrameProvider::VsockReadLoop(uint32_t width, uint32_t height) {
+  jpeg_pending_ = false;
+  while (running_.load() && connection_->ReadMessage(next_frame_)) {
+    if (framesizeMatches(width, height, next_frame_)) {
+      std::lock_guard<std::mutex> lock(frame_mutex_);
+      timestamp_ = systemTime();
+      frame_.swap(next_frame_);
+      yuv_frame_updated_.notify_one();
+    } else if (isBlob(next_frame_)) {
+      std::lock_guard<std::mutex> lock(jpeg_mutex_);
+      bool was_pending = jpeg_pending_.exchange(false);
+      if (was_pending) {
+        cached_jpeg_.swap(next_frame_);
+      }
+    } else {
+      ALOGE("%s: Unexpected data of %zu bytes", __FUNCTION__,
+            next_frame_.size());
+    }
+  }
+  if (!connection_->IsConnected()) {
+    ALOGE("%s: Connection closed - exiting", __FUNCTION__);
+    running_ = false;
+  }
+}
+
+}  // namespace cuttlefish
diff --git a/guest/hals/camera/vsock_frame_provider.h b/guest/hals/camera/vsock_frame_provider.h
new file mode 100644
index 0000000..4454d3e
--- /dev/null
+++ b/guest/hals/camera/vsock_frame_provider.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+#pragma once
+#include <android/hardware/graphics/mapper/2.0/IMapper.h>
+#include <atomic>
+#include <mutex>
+#include <thread>
+#include <vector>
+#include "utils/Timers.h"
+#include "vsock_connection.h"
+
+namespace cuttlefish {
+
+using ::android::hardware::graphics::mapper::V2_0::YCbCrLayout;
+
+// VsockFrameProvider reads data from vsock
+// Users can get the data by using copyYUVFrame/copyJpegData methods
+class VsockFrameProvider {
+ public:
+  VsockFrameProvider() = default;
+  ~VsockFrameProvider();
+
+  VsockFrameProvider(const VsockFrameProvider&) = delete;
+  VsockFrameProvider& operator=(const VsockFrameProvider&) = delete;
+
+  void start(std::shared_ptr<cuttlefish::VsockConnection> connection,
+             uint32_t expected_width, uint32_t expected_height);
+  void stop();
+  void requestJpeg();
+  void cancelJpegRequest();
+  bool jpegPending() const { return jpeg_pending_.load(); }
+  bool isRunning() const { return running_.load(); }
+  bool waitYUVFrame(unsigned int max_wait_ms);
+  bool copyYUVFrame(uint32_t width, uint32_t height, YCbCrLayout dst);
+  bool copyJpegData(uint32_t size, void* dst);
+
+ private:
+  bool isBlob(const std::vector<char>& blob);
+  bool framesizeMatches(uint32_t width, uint32_t height,
+                        const std::vector<char>& data);
+  void VsockReadLoop(uint32_t expected_width, uint32_t expected_height);
+  std::thread reader_thread_;
+  std::mutex frame_mutex_;
+  std::mutex jpeg_mutex_;
+  std::atomic<nsecs_t> timestamp_;
+  std::atomic<bool> running_;
+  std::atomic<bool> jpeg_pending_;
+  std::vector<char> frame_;
+  std::vector<char> next_frame_;
+  std::vector<char> cached_jpeg_;
+  std::condition_variable yuv_frame_updated_;
+  std::shared_ptr<cuttlefish::VsockConnection> connection_;
+};
+
+}  // namespace cuttlefish
diff --git a/guest/hals/ril/reference-libril/Android.bp b/guest/hals/ril/reference-libril/Android.bp
index db265e3..687f95f 100644
--- a/guest/hals/ril/reference-libril/Android.bp
+++ b/guest/hals/ril/reference-libril/Android.bp
@@ -59,3 +59,12 @@
         "libprotobuf-c-nano-enable_malloc"
     ],
 }
+
+filegroup {
+    name: "libril-modem-lib-manifests",
+    srcs: [
+        "[email protected]",
+        "[email protected]",
+    ],
+}
+
diff --git a/guest/hals/ril/reference-libril/[email protected] b/guest/hals/ril/reference-libril/[email protected]
new file mode 100644
index 0000000..97bf66d
--- /dev/null
+++ b/guest/hals/ril/reference-libril/[email protected]
@@ -0,0 +1,11 @@
+<manifest version="1.0" type="device">
+    <hal format="hidl">
+        <name>android.hardware.radio.config</name>
+        <transport>hwbinder</transport>
+        <version>1.3</version>
+        <interface>
+            <name>IRadioConfig</name>
+            <instance>default</instance>
+        </interface>
+    </hal>
+</manifest>
diff --git a/guest/hals/ril/reference-libril/[email protected] b/guest/hals/ril/reference-libril/[email protected]
new file mode 100644
index 0000000..8652229
--- /dev/null
+++ b/guest/hals/ril/reference-libril/[email protected]
@@ -0,0 +1,18 @@
+<manifest version="1.0" type="device">
+    <hal format="hidl">
+        <name>android.hardware.radio</name>
+        <transport>hwbinder</transport>
+        <version>1.6</version>
+        <interface>
+            <name>IRadio</name>
+            <instance>slot1</instance>
+            <!-- cuttlefish doesn't support SIM slot 2/3 -->
+        </interface>
+        <!-- TODO (b/130079344):
+        <interface>
+            <name>ISap</name>
+            <instance>slot1</instance>
+        </interface>
+        -->
+    </hal>
+</manifest>
diff --git a/guest/hals/ril/reference-ril/reference-ril.c b/guest/hals/ril/reference-ril/reference-ril.c
index de040c8..5d17ab4 100644
--- a/guest/hals/ril/reference-ril/reference-ril.c
+++ b/guest/hals/ril/reference-ril/reference-ril.c
@@ -378,6 +378,9 @@
 static bool s_stkServiceRunning = false;
 static char *s_stkUnsolResponse = NULL;
 
+// Next available handle for keep alive session
+static uint32_t s_session_handle = 1;
+
 typedef enum {
     STK_UNSOL_EVENT_UNKNOWN,
     STK_UNSOL_EVENT_NOTIFY,
@@ -4213,6 +4216,13 @@
     RIL_onRequestComplete(t, RIL_E_GENERIC_FAILURE, NULL, 0);
 }
 
+static void requestStartKeepalive(RIL_Token t) {
+    RIL_KeepaliveStatus resp;
+    resp.sessionHandle = s_session_handle++;
+    resp.code = KEEPALIVE_ACTIVE;
+    RIL_onRequestComplete(t, RIL_E_SUCCESS, &resp, sizeof(resp));
+}
+
 void getConfigSlotStatus(RIL_SimSlotStatus_V1_2 *pSimSlotStatus) {
     if (pSimSlotStatus == NULL) {
         return;
@@ -4235,6 +4245,16 @@
     pSimSlotStatus->eid = "";
 }
 
+void sendUnsolNetworkScanResult() {
+    RIL_NetworkScanResult scanr;
+    memset(&scanr, 0, sizeof(scanr));
+    scanr.status = COMPLETE;
+    scanr.error = RIL_E_SUCCESS;
+    scanr.network_infos = NULL;
+    scanr.network_infos_length = 0;
+    RIL_onUnsolicitedResponse(RIL_UNSOL_NETWORK_SCAN_RESULT, &scanr, sizeof(scanr));
+}
+
 void onIccSlotStatus(RIL_Token t) {
     RIL_SimSlotStatus_V1_2 *pSimSlotStatusList =
         (RIL_SimSlotStatus_V1_2 *)calloc(SIM_COUNT, sizeof(RIL_SimSlotStatus_V1_2));
@@ -4857,6 +4877,8 @@
         // New requests after P.
         case RIL_REQUEST_START_NETWORK_SCAN:
             RIL_onRequestComplete(t, RIL_E_SUCCESS, NULL, 0);
+            // send unsol network scan results after a short while
+            RIL_requestTimedCallback (sendUnsolNetworkScanResult, NULL, &TIMEVAL_SIMPOLL);
             break;
         case RIL_REQUEST_GET_MODEM_STACK_STATUS:
             RIL_onRequestComplete(t, RIL_E_SUCCESS, NULL, 0);
@@ -4976,6 +4998,12 @@
         case RIL_REQUEST_SET_DATA_THROTTLING:
             RIL_onRequestComplete(t, RIL_E_SUCCESS, NULL, 0);
             break;
+        case RIL_REQUEST_START_KEEPALIVE:
+            requestStartKeepalive(t);
+            break;
+        case RIL_REQUEST_STOP_KEEPALIVE:
+            RIL_onRequestComplete(t, RIL_E_SUCCESS, NULL, 0);
+            break;
         default:
             RLOGD("Request not supported. Tech: %d",TECH(sMdmInfo));
             RIL_onRequestComplete(t, RIL_E_REQUEST_NOT_SUPPORTED, NULL, 0);
diff --git a/guest/hals/rild/Android.bp b/guest/hals/rild/Android.bp
index 9a7ad01..95dd92d 100644
--- a/guest/hals/rild/Android.bp
+++ b/guest/hals/rild/Android.bp
@@ -39,6 +39,7 @@
         "libril-modem-lib",
     ],
     init_rc: ["rild_cuttlefish.rc"],
+    vintf_fragments: [":libril-modem-lib-manifests"],
     relative_install_path: "hw",
     overrides: ["rild"],
 }
diff --git a/host/commands/assemble_cvd/flags.cc b/host/commands/assemble_cvd/flags.cc
index a1ce5eb..b12dfd2 100644
--- a/host/commands/assemble_cvd/flags.cc
+++ b/host/commands/assemble_cvd/flags.cc
@@ -1,6 +1,7 @@
 #include "host/commands/assemble_cvd/flags.h"
 
 #include <android-base/logging.h>
+#include <android-base/parseint.h>
 #include <android-base/strings.h>
 #include <gflags/gflags.h>
 #include <json/json.h>
@@ -16,6 +17,7 @@
 #include <regex>
 #include <set>
 #include <sstream>
+#include <unordered_map>
 
 #include "common/libs/utils/environment.h"
 #include "common/libs/utils/files.h"
@@ -54,6 +56,26 @@
              "kernel must have been built with CONFIG_RANDOMIZE_BASE "
              "disabled.");
 
+constexpr const char kDisplayHelp[] =
+    "Comma separated key=value pairs of display properties. Supported "
+    "properties:\n"
+    " 'width': required, width of the display in pixels\n"
+    " 'height': required, height of the display in pixels\n"
+    " 'dpi': optional, default 320, density of the display\n"
+    " 'refresh_rate_hz': optional, default 60, display refresh rate in Hertz\n"
+    ". Example usage: \n"
+    "--display0=width=1280,height=720\n"
+    "--display1=width=1440,height=900,dpi=480,refresh_rate_hz=30\n";
+
+// TODO(b/192495477): combine these into a single repeatable '--display' flag
+// when assemble_cvd switches to using the new flag parsing library.
+DEFINE_string(display0, "", kDisplayHelp);
+DEFINE_string(display1, "", kDisplayHelp);
+DEFINE_string(display2, "", kDisplayHelp);
+DEFINE_string(display3, "", kDisplayHelp);
+
+// TODO(b/171305898): mark these as deprecated after multi-display is fully
+// enabled.
 DEFINE_int32(x_res, 0, "Width of the screen in pixels");
 DEFINE_int32(y_res, 0, "Height of the screen in pixels");
 DEFINE_int32(dpi, 0, "Pixels per inch for the screen");
@@ -268,8 +290,6 @@
 DEFINE_bool(record_screen, false, "Enable screen recording. "
                                   "Requires --start_webrtc");
 
-DEFINE_bool(ethernet, false, "Enable Ethernet network interface");
-
 DEFINE_bool(smt, false, "Enable simultaneous multithreading (SMT/HT)");
 
 DEFINE_int32(vsock_guest_cid,
@@ -299,6 +319,8 @@
 DEFINE_bool(enable_audio, cuttlefish::HostArch() != cuttlefish::Arch::Arm64,
             "Whether to play or capture audio");
 
+DEFINE_uint32(camera_server_port, 0, "camera vsock port");
+
 DECLARE_string(assembly_dir);
 DECLARE_string(boot_image);
 DECLARE_string(system_image_dir);
@@ -337,6 +359,60 @@
   return stream.str();
 }
 
+std::optional<CuttlefishConfig::DisplayConfig> ParseDisplayConfig(
+    const std::string& flag) {
+  if (flag.empty()) {
+    return std::nullopt;
+  }
+
+  std::unordered_map<std::string, std::string> props;
+
+  const std::vector<std::string> pairs = android::base::Split(flag, ",");
+  for (const std::string& pair : pairs) {
+    const std::vector<std::string> keyvalue = android::base::Split(pair, "=");
+    CHECK_EQ(2, keyvalue.size()) << "Invalid display: " << flag;
+
+    const std::string& prop_key = keyvalue[0];
+    const std::string& prop_val = keyvalue[1];
+    props[prop_key] = prop_val;
+  }
+
+  CHECK(props.find("width") != props.end())
+      << "Display configuration missing 'width' in " << flag;
+  CHECK(props.find("height") != props.end())
+      << "Display configuration missing 'height' in " << flag;
+
+  int display_width;
+  CHECK(android::base::ParseInt(props["width"], &display_width))
+      << "Display configuration invalid 'width' in " << flag;
+
+  int display_height;
+  CHECK(android::base::ParseInt(props["height"], &display_height))
+      << "Display configuration invalid 'height' in " << flag;
+
+  int display_dpi = 320;
+  auto display_dpi_it = props.find("dpi");
+  if (display_dpi_it != props.end()) {
+    CHECK(android::base::ParseInt(display_dpi_it->second, &display_dpi))
+        << "Display configuration invalid 'dpi' in " << flag;
+  }
+
+  int display_refresh_rate_hz = 60;
+  auto display_refresh_rate_hz_it = props.find("refresh_rate_hz");
+  if (display_refresh_rate_hz_it != props.end()) {
+    CHECK(android::base::ParseInt(display_refresh_rate_hz_it->second,
+                                  &display_refresh_rate_hz))
+        << "Display configuration invalid 'refresh_rate_hz' in " << flag;
+  }
+
+  return CuttlefishConfig::DisplayConfig{
+      .width = display_width,
+      .height = display_height,
+      .dpi = display_dpi,
+      .refresh_rate_hz = display_refresh_rate_hz,
+  };
+}
+
 #ifdef __ANDROID__
 void ReadKernelConfig(KernelConfig* kernel_config) {
   // QEMU isn't on Android, so always follow host arch
@@ -408,6 +484,40 @@
   }
   tmp_config_obj.set_vm_manager(FLAGS_vm_manager);
 
+  std::vector<CuttlefishConfig::DisplayConfig> display_configs;
+
+  auto display0 = ParseDisplayConfig(FLAGS_display0);
+  if (display0) {
+    display_configs.push_back(*display0);
+  }
+  auto display1 = ParseDisplayConfig(FLAGS_display1);
+  if (display1) {
+    display_configs.push_back(*display1);
+  }
+  auto display2 = ParseDisplayConfig(FLAGS_display2);
+  if (display2) {
+    display_configs.push_back(*display2);
+  }
+  auto display3 = ParseDisplayConfig(FLAGS_display3);
+  if (display3) {
+    display_configs.push_back(*display3);
+  }
+
+  if (FLAGS_x_res > 0 && FLAGS_y_res > 0) {
+    if (display_configs.empty()) {
+      display_configs.push_back({
+          .width = FLAGS_x_res,
+          .height = FLAGS_y_res,
+          .dpi = FLAGS_dpi,
+          .refresh_rate_hz = FLAGS_refresh_rate_hz,
+      });
+    } else {
+      LOG(WARNING) << "Ignoring --x_res and --y_res when --displayN specified.";
+    }
+  }
+
+  tmp_config_obj.set_display_configs(display_configs);
+
   const GraphicsAvailability graphics_availability =
     GetGraphicsAvailabilityWithSubprocessCheck();
 
@@ -423,8 +533,8 @@
   }
   if (tmp_config_obj.gpu_mode() == kGpuModeAuto) {
     if (ShouldEnableAcceleratedRendering(graphics_availability)) {
-        LOG(INFO) << "GPU auto mode: detected prerequisites for accelerated "
-                     "rendering support.";
+      LOG(INFO) << "GPU auto mode: detected prerequisites for accelerated "
+                   "rendering support.";
       if (FLAGS_vm_manager == QemuManager::name()) {
         LOG(INFO) << "Enabling --gpu_mode=drm_virgl.";
         tmp_config_obj.set_gpu_mode(kGpuModeDrmVirgl);
@@ -469,14 +579,6 @@
 
   tmp_config_obj.set_setupwizard_mode(FLAGS_setupwizard_mode);
 
-  std::vector<cuttlefish::CuttlefishConfig::DisplayConfig> display_configs = {{
-    .width = FLAGS_x_res,
-    .height = FLAGS_y_res,
-  }};
-  tmp_config_obj.set_display_configs(display_configs);
-  tmp_config_obj.set_dpi(FLAGS_dpi);
-  tmp_config_obj.set_refresh_rate_hz(FLAGS_refresh_rate_hz);
-
   auto secure_hals = android::base::Split(FLAGS_secure_hals, ",");
   tmp_config_obj.set_secure_hals(
       std::set<std::string>(secure_hals.begin(), secure_hals.end()));
@@ -616,8 +718,6 @@
 
   tmp_config_obj.set_record_screen(FLAGS_record_screen);
 
-  tmp_config_obj.set_ethernet(FLAGS_ethernet);
-
   tmp_config_obj.set_enable_host_bluetooth(FLAGS_enable_host_bluetooth);
 
   tmp_config_obj.set_protected_vm(FLAGS_protected_vm);
@@ -701,6 +801,7 @@
     instance.set_rootcanal_default_commands_file(
         FLAGS_bluetooth_default_commands_file);
 
+    instance.set_camera_server_port(FLAGS_camera_server_port);
     instance.set_device_title(FLAGS_device_title);
 
     if (FLAGS_protected_vm) {
diff --git a/host/commands/kernel_log_monitor/kernel_log_server.cc b/host/commands/kernel_log_monitor/kernel_log_server.cc
index 66fafab..03fc90d 100644
--- a/host/commands/kernel_log_monitor/kernel_log_server.cc
+++ b/host/commands/kernel_log_monitor/kernel_log_server.cc
@@ -41,10 +41,13 @@
     {cuttlefish::kMobileNetworkConnectedMessage,
      monitor::Event::MobileNetworkConnected},
     {cuttlefish::kWifiConnectedMessage, monitor::Event::WifiNetworkConnected},
-    {cuttlefish::kEthernetConnectedMessage, monitor::Event::EthernetNetworkConnected},
+    {cuttlefish::kEthernetConnectedMessage,
+     monitor::Event::EthernetNetworkConnected},
     // TODO(b/131864854): Replace this with a string less likely to change
     {"init: starting service 'adbd'...", monitor::Event::AdbdStarted},
     {cuttlefish::kScreenChangedMessage, monitor::Event::ScreenChanged},
+    {cuttlefish::kDisplayPowerModeChangedMessage,
+     monitor::Event::DisplayPowerModeChanged},
 };
 
 void ProcessSubscriptions(
diff --git a/host/commands/kernel_log_monitor/kernel_log_server.h b/host/commands/kernel_log_monitor/kernel_log_server.h
index 659a372..dab675d 100644
--- a/host/commands/kernel_log_monitor/kernel_log_server.h
+++ b/host/commands/kernel_log_monitor/kernel_log_server.h
@@ -37,6 +37,7 @@
   AdbdStarted = 5,
   ScreenChanged = 6,
   EthernetNetworkConnected = 7,
+  DisplayPowerModeChanged = 9,
 };
 
 enum class SubscriptionAction {
diff --git a/host/commands/run_cvd/launch_streamer.cpp b/host/commands/run_cvd/launch_streamer.cpp
index bf0d77d..7ae3f1b 100644
--- a/host/commands/run_cvd/launch_streamer.cpp
+++ b/host/commands/run_cvd/launch_streamer.cpp
@@ -44,26 +44,37 @@
 // Creates the frame and input sockets and add the relevant arguments to the vnc
 // server and webrtc commands
 void CreateStreamerServers(Command* cmd, const CuttlefishConfig& config) {
-  SharedFD touch_server;
+  std::vector<SharedFD> touch_servers;
   SharedFD keyboard_server;
 
   auto instance = config.ForDefaultInstance();
-  if (config.vm_manager() == vm_manager::QemuManager::name()) {
+  auto use_vsockets = config.vm_manager() == vm_manager::QemuManager::name();
+  for (int i = 0; i < config.display_configs().size(); ++i) {
+    touch_servers.push_back(
+        use_vsockets
+            ? SharedFD::VsockServer(instance.touch_server_port(), SOCK_STREAM)
+            : CreateUnixInputServer(instance.touch_socket_path(i)));
+    if (!touch_servers.back()->IsOpen()) {
+      LOG(ERROR) << "Could not open touch server: "
+                 << touch_servers.back()->StrError();
+      return;
+    }
+  }
+  if (!touch_servers.empty()) {
+    cmd->AddParameter("-touch_fds=", touch_servers[0]);
+    for (int i = 1; i < touch_servers.size(); ++i) {
+      cmd->AppendToLastParameter(",", touch_servers[i]);
+    }
+  }
+
+  if (use_vsockets) {
     cmd->AddParameter("-write_virtio_input");
 
-    touch_server =
-        SharedFD::VsockServer(instance.touch_server_port(), SOCK_STREAM);
     keyboard_server =
         SharedFD::VsockServer(instance.keyboard_server_port(), SOCK_STREAM);
   } else {
-    touch_server = CreateUnixInputServer(instance.touch_socket_path());
     keyboard_server = CreateUnixInputServer(instance.keyboard_socket_path());
   }
-  if (!touch_server->IsOpen()) {
-    LOG(ERROR) << "Could not open touch server: " << touch_server->StrError();
-    return;
-  }
-  cmd->AddParameter("-touch_fd=", touch_server);
 
   if (!keyboard_server->IsOpen()) {
     LOG(ERROR) << "Could not open keyboard server: "
diff --git a/host/commands/run_cvd/main.cc b/host/commands/run_cvd/main.cc
index f40226a..944f76c 100644
--- a/host/commands/run_cvd/main.cc
+++ b/host/commands/run_cvd/main.cc
@@ -238,8 +238,7 @@
   } else if (used_tap_devices.count(instance.mobile_tap_name())) {
     LOG(ERROR) << "Mobile TAP device already in use";
     return RunnerExitCodes::kTapDeviceInUse;
-  } else if (config->ethernet() &&
-             used_tap_devices.count(instance.ethernet_tap_name())) {
+  } else if (used_tap_devices.count(instance.ethernet_tap_name())) {
     LOG(ERROR) << "Ethernet TAP device already in use";
   }
 
diff --git a/host/commands/secure_env/tpm_attestation_record.cpp b/host/commands/secure_env/tpm_attestation_record.cpp
index f47a0ef..75a2920 100644
--- a/host/commands/secure_env/tpm_attestation_record.cpp
+++ b/host/commands/secure_env/tpm_attestation_record.cpp
@@ -16,11 +16,20 @@
 #include "host/commands/secure_env/tpm_attestation_record.h"
 
 #include <keymaster/contexts/soft_attestation_cert.h>
+#include <keymaster/km_openssl/attestation_record.h>
+
+#include <openssl/rand.h>
 
 #include <android-base/logging.h>
 
 using keymaster::AuthorizationSet;
 
+TpmAttestationRecordContext::TpmAttestationRecordContext()
+    : keymaster::AttestationContext(::keymaster::KmVersion::KEYMINT_1),
+      unique_id_hbk_(16) {
+  RAND_bytes(unique_id_hbk_.data(), unique_id_hbk_.size());
+}
+
 keymaster_security_level_t TpmAttestationRecordContext::GetSecurityLevel() const {
   return KM_SECURITY_LEVEL_TRUSTED_ENVIRONMENT;
 }
@@ -38,10 +47,11 @@
 }
 
 keymaster::Buffer TpmAttestationRecordContext::GenerateUniqueId(
-    uint64_t, const keymaster_blob_t&, bool, keymaster_error_t* error) const {
-  LOG(ERROR) << "TODO(schuffelen): Implement GenerateUniqueId";
-  *error = KM_ERROR_UNIMPLEMENTED;
-  return {};
+    uint64_t creation_date_time, const keymaster_blob_t& application_id,
+    bool reset_since_rotation, keymaster_error_t* error) const {
+  *error = KM_ERROR_OK;
+  return keymaster::generate_unique_id(unique_id_hbk_, creation_date_time,
+                                       application_id, reset_since_rotation);
 }
 
 const keymaster::AttestationContext::VerifiedBootParams*
diff --git a/host/commands/secure_env/tpm_attestation_record.h b/host/commands/secure_env/tpm_attestation_record.h
index 01e31d7..1609351 100644
--- a/host/commands/secure_env/tpm_attestation_record.h
+++ b/host/commands/secure_env/tpm_attestation_record.h
@@ -16,13 +16,13 @@
 #pragma once
 
 #include <memory>
+#include <vector>
 
 #include <keymaster/attestation_context.h>
 
 class TpmAttestationRecordContext : public keymaster::AttestationContext {
 public:
- TpmAttestationRecordContext()
-     : keymaster::AttestationContext(::keymaster::KmVersion::KEYMINT_1) {}
+ TpmAttestationRecordContext();
  ~TpmAttestationRecordContext() = default;
 
  keymaster_security_level_t GetSecurityLevel() const override;
@@ -40,4 +40,5 @@
 
 private:
     mutable std::unique_ptr<VerifiedBootParams> vb_params_;
+    std::vector<uint8_t> unique_id_hbk_;
 };
diff --git a/host/frontend/vnc_server/Android.bp b/host/frontend/vnc_server/Android.bp
index bfb5561..468590b 100644
--- a/host/frontend/vnc_server/Android.bp
+++ b/host/frontend/vnc_server/Android.bp
@@ -51,8 +51,9 @@
         "libffi",
         "libjpeg",
         "libgflags",
-        "libwayland_server",
+        "libwayland_crosvm_gpu_display_extension_server_protocols",
         "libwayland_extension_server_protocols",
+        "libwayland_server",
     ],
     defaults: ["cuttlefish_host"],
 }
diff --git a/host/frontend/vnc_server/simulated_hw_composer.cpp b/host/frontend/vnc_server/simulated_hw_composer.cpp
index 15bcd8b..a02ae4b 100644
--- a/host/frontend/vnc_server/simulated_hw_composer.cpp
+++ b/host/frontend/vnc_server/simulated_hw_composer.cpp
@@ -73,7 +73,9 @@
 
 SimulatedHWComposer::GenerateProcessedFrameCallback
 SimulatedHWComposer::GetScreenConnectorCallback() {
-  return [](std::uint32_t display_number, std::uint8_t* frame_pixels,
+  return [](std::uint32_t display_number, std::uint32_t frame_w,
+            std::uint32_t frame_h, std::uint32_t frame_stride_bytes,
+            std::uint8_t* frame_bytes,
             cuttlefish::vnc::VncScProcessedFrame& processed_frame) {
     processed_frame.display_number_ = display_number;
     // TODO(171305898): handle multiple displays.
@@ -86,32 +88,25 @@
       // here is incorrectly assuming  surface id == display id.
       display_number = 0;
     }
-    const std::uint32_t display_w =
-        ScreenConnector::ScreenWidth(display_number);
-    const std::uint32_t display_h =
-        ScreenConnector::ScreenHeight(display_number);
-    const std::uint32_t display_stride_bytes =
-        ScreenConnector::ScreenStrideBytes(display_number);
-    const std::uint32_t display_bpp = ScreenConnector::BytesPerPixel();
-    const std::uint32_t display_size_bytes =
-        ScreenConnector::ScreenSizeInBytes(display_number);
+
+    const std::uint32_t frame_bpp = 4;
+    const std::uint32_t frame_size_bytes = frame_h * frame_stride_bytes;
 
     auto& raw_screen = processed_frame.raw_screen_;
-    raw_screen.assign(frame_pixels, frame_pixels + display_size_bytes);
+    raw_screen.assign(frame_bytes, frame_bytes + frame_size_bytes);
 
     static std::uint32_t next_frame_number = 0;
 
     const auto num_stripes = SimulatedHWComposer::kNumStripes;
     for (int i = 0; i < num_stripes; ++i) {
-      std::uint16_t y = (display_h / num_stripes) * i;
+      std::uint16_t y = (frame_h / num_stripes) * i;
 
       // Last frames on the right and/or bottom handle extra pixels
       // when a screen dimension is not evenly divisible by Frame::kNumSlots.
-      std::uint16_t height =
-          display_h / num_stripes +
-          (i + 1 == num_stripes ? display_h % num_stripes : 0);
-      const auto* raw_start = &raw_screen[y * display_w * display_bpp];
-      const auto* raw_end = raw_start + (height * display_w * display_bpp);
+      std::uint16_t height = frame_h / num_stripes +
+                             (i + 1 == num_stripes ? frame_h % num_stripes : 0);
+      const auto* raw_start = &raw_screen[y * frame_w * frame_bpp];
+      const auto* raw_end = raw_start + (height * frame_w * frame_bpp);
       // creating a named object and setting individual data members in order
       // to make klp happy
       // TODO (haining) construct this inside the call when not compiling
@@ -120,8 +115,8 @@
       s.index = i;
       s.x = 0;
       s.y = y;
-      s.width = display_w;
-      s.stride = display_stride_bytes;
+      s.width = frame_w;
+      s.stride = frame_stride_bytes;
       s.height = height;
       s.frame_id = next_frame_number++;
       s.raw_data.assign(raw_start, raw_end);
diff --git a/host/frontend/vnc_server/virtual_inputs.cpp b/host/frontend/vnc_server/virtual_inputs.cpp
index 35fda80..4876338 100644
--- a/host/frontend/vnc_server/virtual_inputs.cpp
+++ b/host/frontend/vnc_server/virtual_inputs.cpp
@@ -16,8 +16,9 @@
 
 #include "host/frontend/vnc_server/virtual_inputs.h"
 
-#include <gflags/gflags.h>
 #include <android-base/logging.h>
+#include <android-base/strings.h>
+#include <gflags/gflags.h>
 #include <linux/input.h>
 #include <linux/uinput.h>
 
@@ -33,8 +34,8 @@
 
 using cuttlefish::vnc::VirtualInputs;
 
-DEFINE_int32(touch_fd, -1,
-             "A fd for a socket where to accept touch connections");
+DEFINE_string(touch_fds, "",
+              "A list of fds for sockets where to accept touch connections");
 
 DEFINE_int32(keyboard_fd, -1,
              "A fd for a socket where to accept keyboard connections");
@@ -315,9 +316,10 @@
   }
 
   void ClientConnectorLoop() {
-    auto touch_server = cuttlefish::SharedFD::Dup(FLAGS_touch_fd);
-    close(FLAGS_touch_fd);
-    FLAGS_touch_fd = -1;
+    auto touch_fd =
+        std::stoi(android::base::Split(FLAGS_touch_fds, ",").front());
+    auto touch_server = cuttlefish::SharedFD::Dup(touch_fd);
+    close(touch_fd);
 
     auto keyboard_server = cuttlefish::SharedFD::Dup(FLAGS_keyboard_fd);
     close(FLAGS_keyboard_fd);
diff --git a/host/frontend/webrtc/Android.bp b/host/frontend/webrtc/Android.bp
index f03a589..42f7fa4 100644
--- a/host/frontend/webrtc/Android.bp
+++ b/host/frontend/webrtc/Android.bp
@@ -22,6 +22,7 @@
     srcs: [
         "lib/audio_device.cpp",
         "lib/audio_track_source_impl.cpp",
+        "lib/camera_streamer.cpp",
         "lib/client_handler.cpp",
         "lib/keyboard.cpp",
         "lib/local_recorder.cpp",
@@ -50,8 +51,9 @@
         "libgflags",
         "libdrm",
         "libffi",
-        "libwayland_server",
+        "libwayland_crosvm_gpu_display_extension_server_protocols",
         "libwayland_extension_server_protocols",
+        "libwayland_server",
         "libwebsockets",
         "libcap",
         "libcuttlefish_utils",
@@ -117,6 +119,7 @@
         "libopus",
         "libsrtp2",
         "libvpx",
+        "libwayland_crosvm_gpu_display_extension_server_protocols",
         "libwayland_extension_server_protocols",
         "libwayland_server",
         "libwebrtc",
diff --git a/host/frontend/webrtc/audio_handler.cpp b/host/frontend/webrtc/audio_handler.cpp
index 4046995..1cd8938 100644
--- a/host/frontend/webrtc/audio_handler.cpp
+++ b/host/frontend/webrtc/audio_handler.cpp
@@ -25,6 +25,28 @@
 namespace cuttlefish {
 namespace {
 
+const virtio_snd_jack_info JACKS[] = {};
+constexpr uint32_t NUM_JACKS = sizeof(JACKS) / sizeof(JACKS[0]);
+
+const virtio_snd_chmap_info CHMAPS[] = {{
+    .hdr = { .hda_fn_nid = Le32(0), },
+    .direction = (uint8_t) AudioStreamDirection::VIRTIO_SND_D_OUTPUT,
+    .channels = 2,
+    .positions = {
+        (uint8_t) AudioChannelMap::VIRTIO_SND_CHMAP_FL,
+        (uint8_t) AudioChannelMap::VIRTIO_SND_CHMAP_FR
+    },
+}, {
+    .hdr = { .hda_fn_nid = Le32(0), },
+    .direction = (uint8_t) AudioStreamDirection::VIRTIO_SND_D_INPUT,
+    .channels = 2,
+    .positions = {
+        (uint8_t) AudioChannelMap::VIRTIO_SND_CHMAP_FL,
+        (uint8_t) AudioChannelMap::VIRTIO_SND_CHMAP_FR
+    },
+}};
+constexpr uint32_t NUM_CHMAPS = sizeof(CHMAPS) / sizeof(CHMAPS[0]);
+
 const virtio_snd_pcm_info STREAMS[] = {{
     .hdr =
         {
@@ -35,10 +57,10 @@
     // formats: It only takes the bits_per_sample as a parameter and assumes
     // the underlying format to be one of the following:
     .formats = Le64(
-        (((uint64_t)1) << (uint8_t)AudioStreamFormat::VIRTIO_SND_PCM_FMT_U8) |
-        (((uint64_t)1) << (uint8_t)AudioStreamFormat::VIRTIO_SND_PCM_FMT_U16) |
-        (((uint64_t)1) << (uint8_t)AudioStreamFormat::VIRTIO_SND_PCM_FMT_U24) |
-        (((uint64_t)1) << (uint8_t)AudioStreamFormat::VIRTIO_SND_PCM_FMT_U32)),
+        (((uint64_t)1) << (uint8_t)AudioStreamFormat::VIRTIO_SND_PCM_FMT_S8) |
+        (((uint64_t)1) << (uint8_t)AudioStreamFormat::VIRTIO_SND_PCM_FMT_S16) |
+        (((uint64_t)1) << (uint8_t)AudioStreamFormat::VIRTIO_SND_PCM_FMT_S24) |
+        (((uint64_t)1) << (uint8_t)AudioStreamFormat::VIRTIO_SND_PCM_FMT_S32)),
     .rates = Le64(
         (((uint64_t)1) << (uint8_t)AudioStreamRate::VIRTIO_SND_PCM_RATE_5512) |
         (((uint64_t)1) << (uint8_t)AudioStreamRate::VIRTIO_SND_PCM_RATE_8000) |
@@ -70,10 +92,10 @@
     // formats: It only takes the bits_per_sample as a parameter and assumes
     // the underlying format to be one of the following:
     .formats = Le64(
-        (((uint64_t)1) << (uint8_t)AudioStreamFormat::VIRTIO_SND_PCM_FMT_U8) |
-        (((uint64_t)1) << (uint8_t)AudioStreamFormat::VIRTIO_SND_PCM_FMT_U16) |
-        (((uint64_t)1) << (uint8_t)AudioStreamFormat::VIRTIO_SND_PCM_FMT_U24) |
-        (((uint64_t)1) << (uint8_t)AudioStreamFormat::VIRTIO_SND_PCM_FMT_U32)),
+        (((uint64_t)1) << (uint8_t)AudioStreamFormat::VIRTIO_SND_PCM_FMT_S8) |
+        (((uint64_t)1) << (uint8_t)AudioStreamFormat::VIRTIO_SND_PCM_FMT_S16) |
+        (((uint64_t)1) << (uint8_t)AudioStreamFormat::VIRTIO_SND_PCM_FMT_S24) |
+        (((uint64_t)1) << (uint8_t)AudioStreamFormat::VIRTIO_SND_PCM_FMT_S32)),
     .rates = Le64(
         (((uint64_t)1) << (uint8_t)AudioStreamRate::VIRTIO_SND_PCM_RATE_5512) |
         (((uint64_t)1) << (uint8_t)AudioStreamRate::VIRTIO_SND_PCM_RATE_8000) |
@@ -271,7 +293,7 @@
 [[noreturn]] void AudioHandler::Loop() {
   for (;;) {
     auto audio_client = audio_server_->AcceptClient(
-        NUM_STREAMS, 0 /* num_jacks, */, 0 /* num_chmaps, */,
+        NUM_STREAMS, NUM_JACKS, NUM_CHMAPS,
         262144 /* tx_shm_len */, 262144 /* rx_shm_len */);
     CHECK(audio_client) << "Failed to create audio client connection instance";
 
@@ -362,6 +384,28 @@
   cmd.Reply(AudioStatus::VIRTIO_SND_S_OK);
 }
 
+void AudioHandler::ChmapsInfo(ChmapInfoCommand& cmd) {
+  if (cmd.start_id() >= NUM_CHMAPS ||
+      cmd.start_id() + cmd.count() > NUM_CHMAPS) {
+    cmd.Reply(AudioStatus::VIRTIO_SND_S_BAD_MSG, {});
+    return;
+  }
+  std::vector<virtio_snd_chmap_info> chmap_info(
+      &CHMAPS[cmd.start_id()], &CHMAPS[cmd.start_id()] + cmd.count());
+  cmd.Reply(AudioStatus::VIRTIO_SND_S_OK, chmap_info);
+}
+
+void AudioHandler::JacksInfo(JackInfoCommand& cmd) {
+  if (cmd.start_id() >= NUM_JACKS ||
+      cmd.start_id() + cmd.count() > NUM_JACKS) {
+    cmd.Reply(AudioStatus::VIRTIO_SND_S_BAD_MSG, {});
+    return;
+  }
+  std::vector<virtio_snd_jack_info> jack_info(
+      &JACKS[cmd.start_id()], &JACKS[cmd.start_id()] + cmd.count());
+  cmd.Reply(AudioStatus::VIRTIO_SND_S_OK, jack_info);
+}
+
 void AudioHandler::OnPlaybackBuffer(TxBuffer buffer) {
   auto stream_id = buffer.stream_id();
   auto& stream_desc = stream_descs_[stream_id];
diff --git a/host/frontend/webrtc/audio_handler.h b/host/frontend/webrtc/audio_handler.h
index 3802052..4da645c 100644
--- a/host/frontend/webrtc/audio_handler.h
+++ b/host/frontend/webrtc/audio_handler.h
@@ -63,6 +63,8 @@
   void ReleaseStream(StreamControlCommand& cmd) override;
   void StartStream(StreamControlCommand& cmd) override;
   void StopStream(StreamControlCommand& cmd) override;
+  void ChmapsInfo(ChmapInfoCommand& cmd) override;
+  void JacksInfo(JackInfoCommand& cmd) override;
 
   void OnPlaybackBuffer(TxBuffer buffer) override;
   void OnCaptureBuffer(RxBuffer buffer) override;
diff --git a/host/frontend/webrtc/connection_observer.cpp b/host/frontend/webrtc/connection_observer.cpp
index 9f11544..0975faa 100644
--- a/host/frontend/webrtc/connection_observer.cpp
+++ b/host/frontend/webrtc/connection_observer.cpp
@@ -35,6 +35,7 @@
 #include "common/libs/fs/shared_buf.h"
 #include "host/frontend/webrtc/adb_handler.h"
 #include "host/frontend/webrtc/bluetooth_handler.h"
+#include "host/frontend/webrtc/lib/camera_controller.h"
 #include "host/frontend/webrtc/lib/utils.h"
 #include "host/libs/config/cuttlefish_config.h"
 
@@ -103,11 +104,13 @@
       cuttlefish::KernelLogEventsHandler *kernel_log_events_handler,
       std::map<std::string, cuttlefish::SharedFD>
           commands_to_custom_action_servers,
-      std::weak_ptr<DisplayHandler> display_handler)
+      std::weak_ptr<DisplayHandler> display_handler,
+      CameraController *camera_controller)
       : input_sockets_(input_sockets),
         kernel_log_events_handler_(kernel_log_events_handler),
         commands_to_custom_action_servers_(commands_to_custom_action_servers),
-        weak_display_handler_(display_handler) {}
+        weak_display_handler_(display_handler),
+        camera_controller_(camera_controller) {}
   virtual ~ConnectionObserverForAndroid() {
     auto display_handler = weak_display_handler_.lock();
     if (display_handler) {
@@ -141,67 +144,65 @@
     }
     }
 
-  void OnTouchEvent(const std::string & /*display_label*/, int x, int y,
-                    bool down) override {
-
-    auto buffer = GetEventBuffer();
-    if (!buffer) {
-      LOG(ERROR) << "Failed to allocate event buffer";
-      return;
-    }
-    buffer->AddEvent(EV_ABS, ABS_X, x);
-    buffer->AddEvent(EV_ABS, ABS_Y, y);
-    buffer->AddEvent(EV_KEY, BTN_TOUCH, down);
-    buffer->AddEvent(EV_SYN, SYN_REPORT, 0);
-    cuttlefish::WriteAll(input_sockets_.touch_client,
-                         reinterpret_cast<const char *>(buffer->data()),
-                         buffer->size());
-  }
-
-  void OnMultiTouchEvent(const std::string & /*display_label*/, Json::Value id,
-                         Json::Value slot, Json::Value x, Json::Value y,
-                         bool down, int size) override {
-
-    auto buffer = GetEventBuffer();
-    if (!buffer) {
-      LOG(ERROR) << "Failed to allocate event buffer";
-      return;
+    void OnTouchEvent(const std::string &display_label, int x, int y,
+                      bool down) override {
+      auto buffer = GetEventBuffer();
+      if (!buffer) {
+        LOG(ERROR) << "Failed to allocate event buffer";
+        return;
+      }
+      buffer->AddEvent(EV_ABS, ABS_X, x);
+      buffer->AddEvent(EV_ABS, ABS_Y, y);
+      buffer->AddEvent(EV_KEY, BTN_TOUCH, down);
+      buffer->AddEvent(EV_SYN, SYN_REPORT, 0);
+      cuttlefish::WriteAll(input_sockets_.GetTouchClientByLabel(display_label),
+                           reinterpret_cast<const char *>(buffer->data()),
+                           buffer->size());
     }
 
-    for (int i=0; i<size; i++) {
-      auto this_slot = slot[i].asInt();
-      auto this_id = id[i].asInt();
-      auto this_x = x[i].asInt();
-      auto this_y = y[i].asInt();
-      buffer->AddEvent(EV_ABS, ABS_MT_SLOT, this_slot);
-      if (down) {
-        bool is_new = active_touch_slots_.insert(this_slot).second;
-        if (is_new) {
+    void OnMultiTouchEvent(const std::string &display_label, Json::Value id,
+                           Json::Value slot, Json::Value x, Json::Value y,
+                           bool down, int size) override {
+      auto buffer = GetEventBuffer();
+      if (!buffer) {
+        LOG(ERROR) << "Failed to allocate event buffer";
+        return;
+      }
+
+      for (int i = 0; i < size; i++) {
+        auto this_slot = slot[i].asInt();
+        auto this_id = id[i].asInt();
+        auto this_x = x[i].asInt();
+        auto this_y = y[i].asInt();
+        buffer->AddEvent(EV_ABS, ABS_MT_SLOT, this_slot);
+        if (down) {
+          bool is_new = active_touch_slots_.insert(this_slot).second;
+          if (is_new) {
+            buffer->AddEvent(EV_ABS, ABS_MT_TRACKING_ID, this_id);
+            if (active_touch_slots_.size() == 1) {
+              buffer->AddEvent(EV_KEY, BTN_TOUCH, 1);
+            }
+          }
+          buffer->AddEvent(EV_ABS, ABS_MT_POSITION_X, this_x);
+          buffer->AddEvent(EV_ABS, ABS_MT_POSITION_Y, this_y);
+          // send ABS_X and ABS_Y for single-touch compatibility
+          buffer->AddEvent(EV_ABS, ABS_X, this_x);
+          buffer->AddEvent(EV_ABS, ABS_Y, this_y);
+        } else {
+          // released touch
           buffer->AddEvent(EV_ABS, ABS_MT_TRACKING_ID, this_id);
-          if (active_touch_slots_.size() == 1) {
-            buffer->AddEvent(EV_KEY, BTN_TOUCH, 1);
+          active_touch_slots_.erase(this_slot);
+          if (active_touch_slots_.empty()) {
+            buffer->AddEvent(EV_KEY, BTN_TOUCH, 0);
           }
         }
-        buffer->AddEvent(EV_ABS, ABS_MT_POSITION_X, this_x);
-        buffer->AddEvent(EV_ABS, ABS_MT_POSITION_Y, this_y);
-        // send ABS_X and ABS_Y for single-touch compatibility
-        buffer->AddEvent(EV_ABS, ABS_X, this_x);
-        buffer->AddEvent(EV_ABS, ABS_Y, this_y);
-      } else {
-        // released touch
-        buffer->AddEvent(EV_ABS, ABS_MT_TRACKING_ID, this_id);
-        active_touch_slots_.erase(this_slot);
-        if (active_touch_slots_.empty()) {
-          buffer->AddEvent(EV_KEY, BTN_TOUCH, 0);
-        }
       }
-    }
 
-    buffer->AddEvent(EV_SYN, SYN_REPORT, 0);
-    cuttlefish::WriteAll(input_sockets_.touch_client,
-                         reinterpret_cast<const char *>(buffer->data()),
-                         buffer->size());
-  }
+      buffer->AddEvent(EV_SYN, SYN_REPORT, 0);
+      cuttlefish::WriteAll(input_sockets_.GetTouchClientByLabel(display_label),
+                           reinterpret_cast<const char *>(buffer->data()),
+                           buffer->size());
+    }
 
   void OnKeyboardEvent(uint16_t code, bool down) override {
     auto buffer = GetEventBuffer();
@@ -244,6 +245,9 @@
   void OnControlChannelOpen(std::function<bool(const Json::Value)>
                             control_message_sender) override {
     LOG(VERBOSE) << "Control Channel open";
+    if (camera_controller_) {
+      camera_controller_->SetMessageSender(control_message_sender);
+    }
     kernel_log_subscription_id_ =
         kernel_log_events_handler_->AddSubscriber(control_message_sender);
   }
@@ -284,6 +288,10 @@
         // Sensor HAL.
       }
       return;
+    } else if (command.rfind("camera_", 0) == 0 && camera_controller_) {
+      // Handle commands starting with "camera_" by camera controller
+      camera_controller_->HandleMessage(evt);
+      return;
     }
 
     auto button_state = evt["button_state"].asString();
@@ -330,6 +338,12 @@
     bluetooth_handler_->handleMessage(msg, size);
   }
 
+  void OnCameraData(const std::vector<char> &data) override {
+    if (camera_controller_) {
+      camera_controller_->HandleMessage(data);
+    }
+  }
+
  private:
   cuttlefish::InputSockets& input_sockets_;
   cuttlefish::KernelLogEventsHandler* kernel_log_events_handler_;
@@ -340,6 +354,7 @@
   std::map<std::string, cuttlefish::SharedFD> commands_to_custom_action_servers_;
   std::weak_ptr<DisplayHandler> weak_display_handler_;
   std::set<int32_t> active_touch_slots_;
+  cuttlefish::CameraController *camera_controller_;
 };
 
 class ConnectionObserverDemuxer
@@ -352,10 +367,12 @@
       std::map<std::string, cuttlefish::SharedFD>
           commands_to_custom_action_servers,
       std::weak_ptr<DisplayHandler> display_handler,
+      CameraController *camera_controller,
       /* params for this class */
       cuttlefish::confui::HostVirtualInput &confui_input)
       : android_input_(input_sockets, kernel_log_events_handler,
-                       commands_to_custom_action_servers, display_handler),
+                       commands_to_custom_action_servers, display_handler,
+                       camera_controller),
         confui_input_{confui_input} {}
   virtual ~ConnectionObserverDemuxer() = default;
 
@@ -433,6 +450,10 @@
     android_input_.OnBluetoothMessage(msg, size);
   }
 
+  void OnCameraData(const std::vector<char> &data) override {
+    android_input_.OnCameraData(data);
+  }
+
  private:
   ConnectionObserverForAndroid android_input_;
   cuttlefish::confui::HostVirtualInput &confui_input_;
@@ -451,7 +472,8 @@
   return std::shared_ptr<cuttlefish::webrtc_streaming::ConnectionObserver>(
       new ConnectionObserverDemuxer(input_sockets_, kernel_log_events_handler_,
                                     commands_to_custom_action_servers_,
-                                    weak_display_handler_, confui_input_));
+                                    weak_display_handler_, camera_controller_,
+                                    confui_input_));
 }
 
 void CfConnectionObserverFactory::AddCustomActionServer(
@@ -467,4 +489,9 @@
     std::weak_ptr<DisplayHandler> display_handler) {
   weak_display_handler_ = display_handler;
 }
+
+void CfConnectionObserverFactory::SetCameraHandler(
+    CameraController *controller) {
+  camera_controller_ = controller;
+}
 }  // namespace cuttlefish
diff --git a/host/frontend/webrtc/connection_observer.h b/host/frontend/webrtc/connection_observer.h
index abf1884..f31472c 100644
--- a/host/frontend/webrtc/connection_observer.h
+++ b/host/frontend/webrtc/connection_observer.h
@@ -22,14 +22,21 @@
 #include "common/libs/fs/shared_fd.h"
 #include "host/frontend/webrtc/display_handler.h"
 #include "host/frontend/webrtc/kernel_log_events_handler.h"
+#include "host/frontend/webrtc/lib/camera_controller.h"
 #include "host/frontend/webrtc/lib/connection_observer.h"
 #include "host/libs/confui/host_virtual_input.h"
 
 namespace cuttlefish {
 
 struct InputSockets {
-  SharedFD touch_server;
-  SharedFD touch_client;
+  SharedFD GetTouchClientByLabel(const std::string& label) {
+    return touch_clients[label];
+  }
+
+  // TODO (b/186773052): Finding strings in a map for every input event may
+  // introduce unwanted latency.
+  std::map<std::string, SharedFD> touch_servers;
+  std::map<std::string, SharedFD> touch_clients;
   SharedFD keyboard_server;
   SharedFD keyboard_client;
   SharedFD switches_server;
@@ -53,6 +60,8 @@
 
   void SetDisplayHandler(std::weak_ptr<DisplayHandler> display_handler);
 
+  void SetCameraHandler(CameraController* controller);
+
  private:
   InputSockets& input_sockets_;
   KernelLogEventsHandler* kernel_log_events_handler_;
@@ -60,6 +69,7 @@
       commands_to_custom_action_servers_;
   std::weak_ptr<DisplayHandler> weak_display_handler_;
   cuttlefish::confui::HostVirtualInput& confui_input_;
+  cuttlefish::CameraController* camera_controller_ = nullptr;
 };
 
 }  // namespace cuttlefish
diff --git a/host/frontend/webrtc/display_handler.cpp b/host/frontend/webrtc/display_handler.cpp
index 1b3bfd8..334c35f 100644
--- a/host/frontend/webrtc/display_handler.cpp
+++ b/host/frontend/webrtc/display_handler.cpp
@@ -24,42 +24,27 @@
 
 namespace cuttlefish {
 DisplayHandler::DisplayHandler(
-    std::shared_ptr<webrtc_streaming::VideoSink> display_sink,
+    std::vector<std::shared_ptr<webrtc_streaming::VideoSink>> display_sinks,
     ScreenConnector& screen_connector)
-    : display_sink_(display_sink), screen_connector_(screen_connector) {
+    : display_sinks_(display_sinks), screen_connector_(screen_connector) {
   screen_connector_.SetCallback(std::move(GetScreenConnectorCallback()));
 }
 
 DisplayHandler::GenerateProcessedFrameCallback DisplayHandler::GetScreenConnectorCallback() {
     // only to tell the producer how to create a ProcessedFrame to cache into the queue
     DisplayHandler::GenerateProcessedFrameCallback callback =
-        [](std::uint32_t display_number, std::uint8_t* frame_pixels,
+        [](std::uint32_t display_number, std::uint32_t frame_width,
+           std::uint32_t frame_height, std::uint32_t frame_stride_bytes,
+           std::uint8_t* frame_pixels,
            WebRtcScProcessedFrame& processed_frame) {
           processed_frame.display_number_ = display_number;
-          // TODO(171305898): handle multiple displays.
-          if (display_number != 0) {
-            // BUG 186580833: display_number comes from surface_id in crosvm
-            // create_surface from virtio_gpu.rs set_scanout.  We cannot use it as
-            // the display number. Either crosvm virtio-gpu is incorrectly ignoring
-            // scanout id and instead using a monotonically increasing surface id
-            // number as the scanout resource is replaced over time, or frontend code
-            // here is incorrectly assuming  surface id == display id.
-            display_number = 0;
-          }
-          const int display_w =
-              ScreenConnectorInfo::ScreenWidth(display_number);
-          const int display_h =
-              ScreenConnectorInfo::ScreenHeight(display_number);
-          const int display_stride_bytes =
-              ScreenConnectorInfo::ScreenStrideBytes(display_number);
-          processed_frame.display_number_ = display_number;
           processed_frame.buf_ =
-              std::make_unique<CvdVideoFrameBuffer>(display_w, display_h);
+              std::make_unique<CvdVideoFrameBuffer>(frame_width, frame_height);
           libyuv::ABGRToI420(
-              frame_pixels, display_stride_bytes, processed_frame.buf_->DataY(),
+              frame_pixels, frame_stride_bytes, processed_frame.buf_->DataY(),
               processed_frame.buf_->StrideY(), processed_frame.buf_->DataU(),
               processed_frame.buf_->StrideU(), processed_frame.buf_->DataV(),
-              processed_frame.buf_->StrideV(), display_w, display_h);
+              processed_frame.buf_->StrideV(), frame_width, frame_height);
           processed_frame.is_success_ = true;
         };
     return callback;
@@ -72,6 +57,7 @@
     {
       std::lock_guard<std::mutex> lock(last_buffer_mutex_);
       std::shared_ptr<CvdVideoFrameBuffer> buffer = std::move(processed_frame.buf_);
+      last_buffer_display_ = processed_frame.display_number_;
       last_buffer_ =
           std::static_pointer_cast<webrtc_streaming::VideoFrameBuffer>(buffer);
     }
@@ -83,9 +69,11 @@
 
 void DisplayHandler::SendLastFrame() {
   std::shared_ptr<webrtc_streaming::VideoFrameBuffer> buffer;
+  std::uint32_t buffer_display;
   {
     std::lock_guard<std::mutex> lock(last_buffer_mutex_);
     buffer = last_buffer_;
+    buffer_display = last_buffer_display_;
   }
   if (!buffer) {
     // If a connection request arrives before the first frame is available don't
@@ -100,7 +88,7 @@
         std::chrono::duration_cast<std::chrono::microseconds>(
             std::chrono::system_clock::now().time_since_epoch())
             .count();
-    display_sink_->OnFrame(buffer, time_stamp);
+    display_sinks_[buffer_display]->OnFrame(buffer, time_stamp);
   }
 }
 
diff --git a/host/frontend/webrtc/display_handler.h b/host/frontend/webrtc/display_handler.h
index aed38a1..7703b08 100644
--- a/host/frontend/webrtc/display_handler.h
+++ b/host/frontend/webrtc/display_handler.h
@@ -16,8 +16,9 @@
 
 #pragma once
 
-#include <mutex>
 #include <memory>
+#include <mutex>
+#include <vector>
 
 #include "host/frontend/webrtc/cvd_video_frame_buffer.h"
 #include "host/frontend/webrtc/lib/video_sink.h"
@@ -51,8 +52,9 @@
   using ScreenConnector = cuttlefish::ScreenConnector<WebRtcScProcessedFrame>;
   using GenerateProcessedFrameCallback = ScreenConnector::GenerateProcessedFrameCallback;
 
-  DisplayHandler(std::shared_ptr<webrtc_streaming::VideoSink> display_sink,
-                 ScreenConnector& screen_connector);
+  DisplayHandler(
+      std::vector<std::shared_ptr<webrtc_streaming::VideoSink>> display_sinks,
+      ScreenConnector& screen_connector);
   ~DisplayHandler() = default;
 
   [[noreturn]] void Loop();
@@ -63,9 +65,10 @@
 
  private:
   GenerateProcessedFrameCallback GetScreenConnectorCallback();
-  std::shared_ptr<webrtc_streaming::VideoSink> display_sink_;
+  std::vector<std::shared_ptr<webrtc_streaming::VideoSink>> display_sinks_;
   ScreenConnector& screen_connector_;
   std::shared_ptr<webrtc_streaming::VideoFrameBuffer> last_buffer_;
+  std::uint32_t last_buffer_display_ = 0;
   std::mutex last_buffer_mutex_;
   std::mutex next_frame_mutex_;
   int client_count_ = 0;
diff --git a/host/frontend/webrtc/kernel_log_events_handler.cpp b/host/frontend/webrtc/kernel_log_events_handler.cpp
index ca14e6f..e31e4da 100644
--- a/host/frontend/webrtc/kernel_log_events_handler.cpp
+++ b/host/frontend/webrtc/kernel_log_events_handler.cpp
@@ -86,6 +86,12 @@
         message["metadata"] = read_result->metadata;
         DeliverEvent(message);
       }
+      if (read_result->event == monitor::Event::DisplayPowerModeChanged) {
+        Json::Value message;
+        message["event"] = kDisplayPowerModeChangedMessage;
+        message["metadata"] = read_result->metadata;
+        DeliverEvent(message);
+      }
     }
   }
 }
diff --git a/host/frontend/webrtc/lib/camera_controller.h b/host/frontend/webrtc/lib/camera_controller.h
new file mode 100644
index 0000000..403af5b
--- /dev/null
+++ b/host/frontend/webrtc/lib/camera_controller.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+#pragma once
+
+#include <json/json.h>
+
+namespace cuttlefish {
+
+class CameraController {
+ public:
+  virtual ~CameraController() = default;
+
+  // Handle data messages coming from the client
+  virtual void HandleMessage(const std::vector<char>& message) = 0;
+  // Handle control messages coming from the client
+  virtual void HandleMessage(const Json::Value& message) = 0;
+  // Send control messages to client
+  virtual void SendMessage(const Json::Value& msg) {
+    if (message_sender_) {
+      message_sender_(msg);
+    }
+  }
+  virtual void SetMessageSender(
+      std::function<bool(const Json::Value& msg)> sender) {
+    message_sender_ = sender;
+  }
+
+ protected:
+  std::function<bool(const Json::Value& msg)> message_sender_;
+};
+
+}  // namespace cuttlefish
diff --git a/host/frontend/webrtc/lib/camera_streamer.cpp b/host/frontend/webrtc/lib/camera_streamer.cpp
new file mode 100644
index 0000000..7634e28
--- /dev/null
+++ b/host/frontend/webrtc/lib/camera_streamer.cpp
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+#include "camera_streamer.h"
+
+#include <android-base/logging.h>
+#include <chrono>
+#include "common/libs/utils/vsock_connection.h"
+
+namespace cuttlefish {
+namespace webrtc_streaming {
+
+CameraStreamer::CameraStreamer(unsigned int port, unsigned int cid)
+    : cid_(cid), port_(port) {}
+
+CameraStreamer::~CameraStreamer() { Disconnect(); }
+
+// We are getting frames from the client so try forwarding those to the CVD
+void CameraStreamer::OnFrame(const webrtc::VideoFrame& client_frame) {
+  std::lock_guard<std::mutex> lock(onframe_mutex_);
+  if (!cvd_connection_.IsConnected() && !pending_connection_.valid()) {
+    // Start new connection
+    pending_connection_ = cvd_connection_.ConnectAsync(port_, cid_);
+    return;
+  } else if (pending_connection_.valid()) {
+    if (!IsConnectionReady()) {
+      return;
+    }
+    std::lock_guard<std::mutex> lock(settings_mutex_);
+    if (!cvd_connection_.WriteMessage(settings_buffer_)) {
+      LOG(ERROR) << "Failed writing camera settings:";
+      return;
+    }
+    StartReadLoop();
+    LOG(INFO) << "Connected!";
+  }
+  auto resolution = resolution_.load();
+  if (resolution.height <= 0 || resolution.width <= 0) {
+    // We don't have a valid resolution that is necessary for
+    // potential frame scaling
+    return;
+  }
+  auto frame = client_frame.video_frame_buffer()->ToI420().get();
+  if (frame->width() != resolution.width ||
+      frame->height() != resolution.height) {
+    // incoming resolution does not match with the resolution we
+    // have communicated to the CVD - scaling required
+    if (!scaled_frame_ || resolution.width != scaled_frame_->width() ||
+        resolution.height != scaled_frame_->height()) {
+      scaled_frame_ =
+          webrtc::I420Buffer::Create(resolution.width, resolution.height);
+    }
+    scaled_frame_->CropAndScaleFrom(*frame);
+    frame = scaled_frame_.get();
+  }
+  if (!VsockSendYUVFrame(frame)) {
+    LOG(ERROR) << "Sending frame over vsock failed";
+  }
+}
+
+// Handle message json coming from client
+void CameraStreamer::HandleMessage(const Json::Value& message) {
+  auto command = message["command"].asString();
+  if (command == "camera_settings") {
+    // save local copy of resolution that is required for frame scaling
+    resolution_ = GetResolutionFromSettings(message);
+    Json::StreamWriterBuilder factory;
+    std::string new_settings = Json::writeString(factory, message);
+    if (!settings_buffer_.empty() && new_settings != settings_buffer_) {
+      // Settings have changed - disconnect
+      // Next incoming frames will trigger re-connection
+      Disconnect();
+    }
+    std::lock_guard<std::mutex> lock(settings_mutex_);
+    settings_buffer_ = new_settings;
+    LOG(INFO) << "New camera settings received:" << new_settings;
+  }
+}
+
+// Handle binary blobs coming from client
+void CameraStreamer::HandleMessage(const std::vector<char>& message) {
+  LOG(INFO) << "Pass through " << message.size() << "bytes";
+  std::lock_guard<std::mutex> lock(frame_mutex_);
+  cvd_connection_.WriteMessage(message);
+}
+
+CameraStreamer::Resolution CameraStreamer::GetResolutionFromSettings(
+    const Json::Value& settings) {
+  return {.width = settings["width"].asInt(),
+          .height = settings["height"].asInt()};
+}
+
+bool CameraStreamer::VsockSendYUVFrame(
+    const webrtc::I420BufferInterface* frame) {
+  int32_t size = frame->width() * frame->height() +
+                 2 * frame->ChromaWidth() * frame->ChromaHeight();
+  const char* y = reinterpret_cast<const char*>(frame->DataY());
+  const char* u = reinterpret_cast<const char*>(frame->DataU());
+  const char* v = reinterpret_cast<const char*>(frame->DataV());
+  auto chroma_width = frame->ChromaWidth();
+  auto chroma_height = frame->ChromaHeight();
+  std::lock_guard<std::mutex> lock(frame_mutex_);
+  return cvd_connection_.Write(size) &&
+         cvd_connection_.WriteStrides(y, frame->width(), frame->height(),
+                                      frame->StrideY()) &&
+         cvd_connection_.WriteStrides(u, chroma_width, chroma_height,
+                                      frame->StrideU()) &&
+         cvd_connection_.WriteStrides(v, chroma_width, chroma_height,
+                                      frame->StrideV());
+}
+
+bool CameraStreamer::IsConnectionReady() {
+  if (!pending_connection_.valid()) {
+    return cvd_connection_.IsConnected();
+  } else if (pending_connection_.wait_for(std::chrono::seconds(0)) !=
+             std::future_status::ready) {
+    // Still waiting for connection
+    return false;
+  } else if (settings_buffer_.empty()) {
+    // connection is ready but we have not yet received client
+    // camera settings
+    return false;
+  }
+  return pending_connection_.get();
+}
+
+void CameraStreamer::StartReadLoop() {
+  if (reader_thread_.joinable()) {
+    reader_thread_.join();
+  }
+  reader_thread_ = std::thread([this] {
+    while (cvd_connection_.IsConnected()) {
+      auto json_value = cvd_connection_.ReadJsonMessage();
+      if (!json_value.empty()) {
+        SendMessage(json_value);
+      }
+    }
+    LOG(INFO) << "Exit reader thread";
+  });
+}
+
+void CameraStreamer::Disconnect() {
+  cvd_connection_.Disconnect();
+  if (reader_thread_.joinable()) {
+    reader_thread_.join();
+  }
+}
+
+}  // namespace webrtc_streaming
+}  // namespace cuttlefish
diff --git a/host/frontend/webrtc/lib/camera_streamer.h b/host/frontend/webrtc/lib/camera_streamer.h
new file mode 100644
index 0000000..ceab2e6
--- /dev/null
+++ b/host/frontend/webrtc/lib/camera_streamer.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+#pragma once
+#include <api/video/i420_buffer.h>
+#include <api/video/video_frame.h>
+#include <api/video/video_sink_interface.h>
+#include <json/json.h>
+
+#include "common/libs/utils/vsock_connection.h"
+#include "host/frontend/webrtc/lib/camera_controller.h"
+
+#include <atomic>
+#include <mutex>
+#include <thread>
+#include <vector>
+
+namespace cuttlefish {
+namespace webrtc_streaming {
+
+class CameraStreamer : public rtc::VideoSinkInterface<webrtc::VideoFrame>,
+                       public CameraController {
+ public:
+  CameraStreamer(unsigned int port, unsigned int cid);
+  ~CameraStreamer();
+
+  CameraStreamer(const CameraStreamer& other) = delete;
+  CameraStreamer& operator=(const CameraStreamer& other) = delete;
+
+  void OnFrame(const webrtc::VideoFrame& frame) override;
+
+  void HandleMessage(const Json::Value& message) override;
+  void HandleMessage(const std::vector<char>& message) override;
+
+ private:
+  using Resolution = struct {
+    int32_t width;
+    int32_t height;
+  };
+  bool ForwardClientMessage(const Json::Value& message);
+  Resolution GetResolutionFromSettings(const Json::Value& settings);
+  bool VsockSendYUVFrame(const webrtc::I420BufferInterface* frame);
+  bool IsConnectionReady();
+  void StartReadLoop();
+  void Disconnect();
+  std::future<bool> pending_connection_;
+  VsockClientConnection cvd_connection_;
+  std::atomic<Resolution> resolution_;
+  std::mutex settings_mutex_;
+  std::string settings_buffer_;
+  std::mutex frame_mutex_;
+  std::mutex onframe_mutex_;
+  rtc::scoped_refptr<webrtc::I420Buffer> scaled_frame_;
+  unsigned int cid_;
+  unsigned int port_;
+  std::thread reader_thread_;
+};
+
+}  // namespace webrtc_streaming
+}  // namespace cuttlefish
diff --git a/host/frontend/webrtc/lib/client_handler.cpp b/host/frontend/webrtc/lib/client_handler.cpp
index 31fd50b..4893f8d 100644
--- a/host/frontend/webrtc/lib/client_handler.cpp
+++ b/host/frontend/webrtc/lib/client_handler.cpp
@@ -39,6 +39,8 @@
 static constexpr auto kInputChannelLabel = "input-channel";
 static constexpr auto kAdbChannelLabel = "adb-channel";
 static constexpr auto kBluetoothChannelLabel = "bluetooth-channel";
+static constexpr auto kCameraDataChannelLabel = "camera-data-channel";
+static constexpr auto kCameraDataEof = "EOF";
 
 class CvdCreateSessionDescriptionObserver
     : public webrtc::CreateSessionDescriptionObserver {
@@ -166,6 +168,22 @@
   bool channel_open_reported_ = false;
 };
 
+class CameraChannelHandler : public webrtc::DataChannelObserver {
+ public:
+  CameraChannelHandler(
+      rtc::scoped_refptr<webrtc::DataChannelInterface> bluetooth_channel,
+      std::shared_ptr<ConnectionObserver> observer);
+  ~CameraChannelHandler() override;
+
+  void OnStateChange() override;
+  void OnMessage(const webrtc::DataBuffer &msg) override;
+
+ private:
+  rtc::scoped_refptr<webrtc::DataChannelInterface> camera_channel_;
+  std::shared_ptr<ConnectionObserver> observer_;
+  std::vector<char> receive_buffer_;
+};
+
 InputChannelHandler::InputChannelHandler(
     rtc::scoped_refptr<webrtc::DataChannelInterface> input_channel,
     std::shared_ptr<ConnectionObserver> observer)
@@ -375,22 +393,53 @@
   observer_->OnBluetoothMessage(msg.data.cdata(), msg.size());
 }
 
+CameraChannelHandler::CameraChannelHandler(
+    rtc::scoped_refptr<webrtc::DataChannelInterface> camera_channel,
+    std::shared_ptr<ConnectionObserver> observer)
+    : camera_channel_(camera_channel), observer_(observer) {
+  camera_channel_->RegisterObserver(this);
+}
+
+CameraChannelHandler::~CameraChannelHandler() {
+  camera_channel_->UnregisterObserver();
+}
+
+void CameraChannelHandler::OnStateChange() {
+  LOG(VERBOSE) << "Camera channel state changed to "
+               << webrtc::DataChannelInterface::DataStateString(
+                      camera_channel_->state());
+}
+
+void CameraChannelHandler::OnMessage(const webrtc::DataBuffer &msg) {
+  auto msg_data = msg.data.cdata<char>();
+  if (msg.size() == strlen(kCameraDataEof) &&
+      !strncmp(msg_data, kCameraDataEof, msg.size())) {
+    // Send complete buffer to observer on EOF marker
+    observer_->OnCameraData(receive_buffer_);
+    receive_buffer_.clear();
+    return;
+  }
+  // Otherwise buffer up data
+  receive_buffer_.insert(receive_buffer_.end(), msg_data,
+                         msg_data + msg.size());
+}
+
 std::shared_ptr<ClientHandler> ClientHandler::Create(
     int client_id, std::shared_ptr<ConnectionObserver> observer,
     std::function<void(const Json::Value &)> send_to_client_cb,
-    std::function<void()> on_connection_closed_cb) {
+    std::function<void(bool)> on_connection_changed_cb) {
   return std::shared_ptr<ClientHandler>(new ClientHandler(
-      client_id, observer, send_to_client_cb, on_connection_closed_cb));
+      client_id, observer, send_to_client_cb, on_connection_changed_cb));
 }
 
 ClientHandler::ClientHandler(
     int client_id, std::shared_ptr<ConnectionObserver> observer,
     std::function<void(const Json::Value &)> send_to_client_cb,
-    std::function<void()> on_connection_closed_cb)
+    std::function<void(bool)> on_connection_changed_cb)
     : client_id_(client_id),
       observer_(observer),
       send_to_client_(send_to_client_cb),
-      on_connection_closed_cb_(on_connection_closed_cb) {}
+      on_connection_changed_cb_(on_connection_changed_cb) {}
 
 ClientHandler::~ClientHandler() {
   for (auto &data_channel : data_channels_) {
@@ -450,6 +499,17 @@
   return true;
 }
 
+webrtc::VideoTrackInterface *ClientHandler::GetCameraStream() const {
+  for (const auto &tranceiver : peer_connection_->GetTransceivers()) {
+    auto track = tranceiver->receiver()->track();
+    if (track &&
+        track->kind() == webrtc::MediaStreamTrackInterface::kVideoKind) {
+      return static_cast<webrtc::VideoTrackInterface *>(track.get());
+    }
+  }
+  return nullptr;
+}
+
 void ClientHandler::LogAndReplyError(const std::string &error_msg) const {
   LOG(ERROR) << error_msg;
   Json::Value reply;
@@ -607,7 +667,7 @@
   // will then wait for the callback to return -> deadlock). Destroying the
   // peer_connection_ has the same effect. The only alternative is to postpone
   // that operation until after the callback returns.
-  on_connection_closed_cb_();
+  on_connection_changed_cb_(false);
 }
 
 void ClientHandler::OnConnectionChange(
@@ -625,6 +685,7 @@
             control_handler_->Send(msg, size, binary);
             return true;
           });
+      on_connection_changed_cb_(true);
       break;
     case webrtc::PeerConnectionInterface::PeerConnectionState::kDisconnected:
       LOG(VERBOSE) << "Client " << client_id_ << ": Connection disconnected";
@@ -667,6 +728,9 @@
   } else if (label == kBluetoothChannelLabel) {
     bluetooth_handler_.reset(
         new BluetoothChannelHandler(data_channel, observer_));
+  } else if (label == kCameraDataChannelLabel) {
+    camera_data_handler_.reset(
+        new CameraChannelHandler(data_channel, observer_));
   } else {
     LOG(VERBOSE) << "Data channel connected: " << label;
     data_channels_.push_back(data_channel);
diff --git a/host/frontend/webrtc/lib/client_handler.h b/host/frontend/webrtc/lib/client_handler.h
index c85f169..f7f587b 100644
--- a/host/frontend/webrtc/lib/client_handler.h
+++ b/host/frontend/webrtc/lib/client_handler.h
@@ -21,6 +21,7 @@
 #include <optional>
 #include <sstream>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include <json/json.h>
@@ -37,6 +38,7 @@
 class AdbChannelHandler;
 class ControlChannelHandler;
 class BluetoothChannelHandler;
+class CameraChannelHandler;
 
 class ClientHandler : public webrtc::PeerConnectionObserver,
                       public std::enable_shared_from_this<ClientHandler> {
@@ -44,7 +46,7 @@
   static std::shared_ptr<ClientHandler> Create(
       int client_id, std::shared_ptr<ConnectionObserver> observer,
       std::function<void(const Json::Value&)> send_client_cb,
-      std::function<void()> on_connection_closed_cb);
+      std::function<void(bool)> on_connection_changed_cb);
   ~ClientHandler() override;
 
   bool SetPeerConnection(
@@ -56,6 +58,8 @@
   bool AddAudio(rtc::scoped_refptr<webrtc::AudioTrackInterface> track,
                   const std::string& label);
 
+  webrtc::VideoTrackInterface* GetCameraStream() const;
+
   void HandleMessage(const Json::Value& client_message);
 
   // CreateSessionDescriptionObserver implementation
@@ -107,7 +111,7 @@
   };
   ClientHandler(int client_id, std::shared_ptr<ConnectionObserver> observer,
                 std::function<void(const Json::Value&)> send_client_cb,
-                std::function<void()> on_connection_closed_cb);
+                std::function<void(bool)> on_connection_changed_cb);
 
   // Intentionally private, disconnect the client by destroying the object.
   void Close();
@@ -118,13 +122,14 @@
   State state_ = State::kNew;
   std::shared_ptr<ConnectionObserver> observer_;
   std::function<void(const Json::Value&)> send_to_client_;
-  std::function<void()> on_connection_closed_cb_;
+  std::function<void(bool)> on_connection_changed_cb_;
   rtc::scoped_refptr<webrtc::PeerConnectionInterface> peer_connection_;
   std::vector<rtc::scoped_refptr<webrtc::DataChannelInterface>> data_channels_;
   std::unique_ptr<InputChannelHandler> input_handler_;
   std::unique_ptr<AdbChannelHandler> adb_handler_;
   std::unique_ptr<ControlChannelHandler> control_handler_;
   std::unique_ptr<BluetoothChannelHandler> bluetooth_handler_;
+  std::unique_ptr<CameraChannelHandler> camera_data_handler_;
 };
 
 }  // namespace webrtc_streaming
diff --git a/host/frontend/webrtc/lib/connection_observer.h b/host/frontend/webrtc/lib/connection_observer.h
index be6fc13..fe82549 100644
--- a/host/frontend/webrtc/lib/connection_observer.h
+++ b/host/frontend/webrtc/lib/connection_observer.h
@@ -45,6 +45,7 @@
   virtual void OnBluetoothChannelOpen(
       std::function<bool(const uint8_t*, size_t)> bluetooth_message_sender) = 0;
   virtual void OnBluetoothMessage(const uint8_t* msg, size_t size) = 0;
+  virtual void OnCameraData(const std::vector<char>& data) = 0;
 };
 
 class ConnectionObserverFactory {
diff --git a/host/frontend/webrtc/lib/streamer.cpp b/host/frontend/webrtc/lib/streamer.cpp
index 7fee1de..71bbab5 100644
--- a/host/frontend/webrtc/lib/streamer.cpp
+++ b/host/frontend/webrtc/lib/streamer.cpp
@@ -34,6 +34,7 @@
 
 #include "host/frontend/webrtc/lib/audio_device.h"
 #include "host/frontend/webrtc/lib/audio_track_source_impl.h"
+#include "host/frontend/webrtc/lib/camera_streamer.h"
 #include "host/frontend/webrtc/lib/client_handler.h"
 #include "host/frontend/webrtc/lib/port_range_socket_factory.h"
 #include "host/frontend/webrtc/lib/video_track_source_impl.h"
@@ -141,6 +142,7 @@
 
   void SendMessageToClient(int client_id, const Json::Value& msg);
   void DestroyClientHandler(int client_id);
+  void SetupCameraForClient(int client_id);
 
   // WsObserver
   void OnOpen() override;
@@ -170,6 +172,7 @@
   std::map<std::string, std::string> hardware_;
   std::vector<ControlPanelButtonDescriptor> custom_control_panel_buttons_;
   std::shared_ptr<AudioDeviceModuleWrapper> audio_device_module_;
+  std::unique_ptr<CameraStreamer> camera_streamer_;
 };
 
 Streamer::Streamer(std::unique_ptr<Streamer::Impl> impl)
@@ -263,6 +266,11 @@
   return impl_->audio_device_module_;
 }
 
+CameraController* Streamer::AddCamera(unsigned int port, unsigned int cid) {
+  impl_->camera_streamer_ = std::make_unique<CameraStreamer>(port, cid);
+  return impl_->camera_streamer_.get();
+}
+
 void Streamer::SetHardwareSpec(std::string key, std::string value) {
   impl_->hardware_.emplace(key, value);
 }
@@ -566,7 +574,13 @@
       [this, client_id](const Json::Value& msg) {
         SendMessageToClient(client_id, msg);
       },
-      [this, client_id] { DestroyClientHandler(client_id); });
+      [this, client_id](bool isOpen) {
+        if (isOpen) {
+          SetupCameraForClient(client_id);
+        } else {
+          DestroyClientHandler(client_id);
+        }
+      });
 
   webrtc::PeerConnectionInterface::RTCConfiguration config;
   config.sdp_semantics = webrtc::SdpSemantics::kUnifiedPlan;
@@ -641,5 +655,19 @@
   });
 }
 
+void Streamer::Impl::SetupCameraForClient(int client_id) {
+  if (!camera_streamer_) {
+    return;
+  }
+  auto client_handler = clients_[client_id];
+  if (client_handler) {
+    auto camera_track = client_handler->GetCameraStream();
+    if (camera_track) {
+      camera_track->AddOrUpdateSink(camera_streamer_.get(),
+                                    rtc::VideoSinkWants());
+    }
+  }
+}
+
 }  // namespace webrtc_streaming
 }  // namespace cuttlefish
diff --git a/host/frontend/webrtc/lib/streamer.h b/host/frontend/webrtc/lib/streamer.h
index 0628e7d..50a7e12 100644
--- a/host/frontend/webrtc/lib/streamer.h
+++ b/host/frontend/webrtc/lib/streamer.h
@@ -28,6 +28,7 @@
 
 #include "host/frontend/webrtc/lib/audio_sink.h"
 #include "host/frontend/webrtc/lib/audio_source.h"
+#include "host/frontend/webrtc/lib/camera_controller.h"
 #include "host/frontend/webrtc/lib/connection_observer.h"
 #include "host/frontend/webrtc/lib/local_recorder.h"
 #include "host/frontend/webrtc/lib/video_sink.h"
@@ -98,6 +99,8 @@
   // stream here.
   std::shared_ptr<AudioSource> GetAudioSource();
 
+  CameraController* AddCamera(unsigned int port, unsigned int cid);
+
   // Add a custom button to the control panel.
   void AddCustomControlPanelButton(const std::string& command,
                                    const std::string& title,
diff --git a/host/frontend/webrtc/main.cpp b/host/frontend/webrtc/main.cpp
index 007c405..57d5644 100644
--- a/host/frontend/webrtc/main.cpp
+++ b/host/frontend/webrtc/main.cpp
@@ -33,8 +33,10 @@
 #include "host/frontend/webrtc/connection_observer.h"
 #include "host/frontend/webrtc/display_handler.h"
 #include "host/frontend/webrtc/kernel_log_events_handler.h"
+#include "host/frontend/webrtc/lib/camera_controller.h"
 #include "host/frontend/webrtc/lib/local_recorder.h"
 #include "host/frontend/webrtc/lib/streamer.h"
+#include "host/frontend/webrtc/lib/video_sink.h"
 #include "host/libs/audio_connector/server.h"
 #include "host/libs/config/cuttlefish_config.h"
 #include "host/libs/config/logging.h"
@@ -42,7 +44,8 @@
 #include "host/libs/confui/host_server.h"
 #include "host/libs/screen_connector/screen_connector.h"
 
-DEFINE_int32(touch_fd, -1, "An fd to listen on for touch connections.");
+DEFINE_string(touch_fds, "",
+              "A list of fds to listen on for touch connections.");
 DEFINE_int32(keyboard_fd, -1, "An fd to listen on for keyboard connections.");
 DEFINE_int32(switches_fd, -1, "An fd to listen on for switch connections.");
 DEFINE_int32(frame_server_fd, -1, "An fd to listen on for frame updates");
@@ -55,6 +58,7 @@
 DEFINE_bool(write_virtio_input, true,
             "Whether to send input events in virtio format.");
 DEFINE_int32(audio_server_fd, -1, "An fd to listen on for audio frames");
+DEFINE_int32(camera_streamer_fd, -1, "An fd to send client camera frames");
 
 using cuttlefish::AudioHandler;
 using cuttlefish::CfConnectionObserverFactory;
@@ -63,6 +67,7 @@
 using cuttlefish::webrtc_streaming::LocalRecorder;
 using cuttlefish::webrtc_streaming::Streamer;
 using cuttlefish::webrtc_streaming::StreamerConfig;
+using cuttlefish::webrtc_streaming::VideoSink;
 
 class CfOperatorObserver
     : public cuttlefish::webrtc_streaming::OperatorObserver {
@@ -132,11 +137,16 @@
 
   cuttlefish::InputSockets input_sockets;
 
-  input_sockets.touch_server = cuttlefish::SharedFD::Dup(FLAGS_touch_fd);
+  auto counter = 0;
+  for (const auto& touch_fd_str : android::base::Split(FLAGS_touch_fds, ",")) {
+    auto touch_fd = std::stoi(touch_fd_str);
+    input_sockets.touch_servers["display_" + std::to_string(counter++)] =
+        cuttlefish::SharedFD::Dup(touch_fd);
+    close(touch_fd);
+  }
   input_sockets.keyboard_server = cuttlefish::SharedFD::Dup(FLAGS_keyboard_fd);
   input_sockets.switches_server = cuttlefish::SharedFD::Dup(FLAGS_switches_fd);
   auto control_socket = cuttlefish::SharedFD::Dup(FLAGS_command_fd);
-  close(FLAGS_touch_fd);
   close(FLAGS_keyboard_fd);
   close(FLAGS_switches_fd);
   close(FLAGS_command_fd);
@@ -145,19 +155,25 @@
   // devices have been initialized. That's OK though, because without those
   // devices there is no meaningful interaction the user can have with the
   // device.
-  input_sockets.touch_client =
-      cuttlefish::SharedFD::Accept(*input_sockets.touch_server);
+  for (const auto& touch_entry : input_sockets.touch_servers) {
+    input_sockets.touch_clients[touch_entry.first] =
+        cuttlefish::SharedFD::Accept(*touch_entry.second);
+  }
   input_sockets.keyboard_client =
       cuttlefish::SharedFD::Accept(*input_sockets.keyboard_server);
   input_sockets.switches_client =
       cuttlefish::SharedFD::Accept(*input_sockets.switches_server);
 
-  std::thread touch_accepter([&input_sockets]() {
-    for (;;) {
-      input_sockets.touch_client =
-          cuttlefish::SharedFD::Accept(*input_sockets.touch_server);
-    }
-  });
+  std::vector<std::thread> touch_accepters;
+  for (const auto& touch : input_sockets.touch_servers) {
+    auto label = touch.first;
+    touch_accepters.emplace_back([label, &input_sockets]() {
+      for (;;) {
+        input_sockets.touch_clients[label] =
+            cuttlefish::SharedFD::Accept(*input_sockets.touch_servers[label]);
+      }
+    });
+  }
   std::thread keyboard_accepter([&input_sockets]() {
     for (;;) {
       input_sockets.keyboard_client =
@@ -213,11 +229,27 @@
   auto streamer = Streamer::Create(streamer_config, observer_factory);
   CHECK(streamer) << "Could not create streamer";
 
-  auto display_0 = streamer->AddDisplay(
-      "display_0", screen_connector.ScreenWidth(0),
-      screen_connector.ScreenHeight(0), cvd_config->dpi(), true);
-  auto display_handler = std::shared_ptr<DisplayHandler>(
-      new DisplayHandler(display_0, screen_connector));
+  uint32_t display_index = 0;
+  std::vector<std::shared_ptr<VideoSink>> displays;
+  for (const auto& display_config : cvd_config->display_configs()) {
+    const std::string display_id = "display_" + std::to_string(display_index);
+
+    auto display =
+        streamer->AddDisplay(display_id, display_config.width,
+                             display_config.height, display_config.dpi, true);
+    displays.push_back(display);
+
+    ++display_index;
+  }
+
+  auto display_handler =
+      std::make_shared<DisplayHandler>(std::move(displays), screen_connector);
+
+  if (instance.camera_server_port()) {
+    auto camera_controller = streamer->AddCamera(instance.camera_server_port(),
+                                                 instance.vsock_guest_cid());
+    observer_factory->SetCameraHandler(camera_controller);
+  }
 
   std::unique_ptr<cuttlefish::webrtc_streaming::LocalRecorder> local_recorder;
   if (cvd_config->record_screen()) {
diff --git a/host/frontend/webrtc_operator/assets/index.html b/host/frontend/webrtc_operator/assets/index.html
index 1eae893..f0dc25e 100644
--- a/host/frontend/webrtc_operator/assets/index.html
+++ b/host/frontend/webrtc_operator/assets/index.html
@@ -22,6 +22,7 @@
         <link rel="stylesheet" type="text/css" href="controls.css" >
         <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined">
         <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
+        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     </head>
 
     <body>
@@ -34,13 +35,14 @@
           <div id='app-controls'>
             <div id="keyboard-capture-control" title="Capture Keyboard"></div>
             <div id="mic-capture-control" title="Capture Microphone"></div>
+            <div id="camera-control" title="Capture Camera"></div>
             <audio autoplay controls id="device-audio"></audio>
           </div>
           <div id='status-div'>
             <h3 id='status-message' class='connecting'>Connecting to device</h3>
           </div>
         </div>
-        <div id='controls-and-screens'>
+        <div id='controls-and-displays'>
           <div id='control-panel-default-buttons' class='control-panel-column'>
             <button id='device-details-button' title='Device Details' class='material-icons'>
               settings
@@ -50,8 +52,7 @@
             </button>
           </div>
           <div id='control-panel-custom-buttons' class='control-panel-column'></div>
-          <div id='screens'>
-            <video id="device-screen" autoplay ></video>
+          <div id='device-displays'>
           </div>
         </div>
       </section>
diff --git a/host/frontend/webrtc_operator/assets/js/app.js b/host/frontend/webrtc_operator/assets/js/app.js
index 441df00..db9c8a7 100644
--- a/host/frontend/webrtc_operator/assets/js/app.js
+++ b/host/frontend/webrtc_operator/assets/js/app.js
@@ -22,9 +22,11 @@
   createToggleControl(keyboardCaptureCtrl, "keyboard", onKeyboardCaptureToggle);
   const micCaptureCtrl = document.getElementById('mic-capture-control');
   createToggleControl(micCaptureCtrl, "mic", onMicCaptureToggle);
+  const cameraCtrl = document.getElementById('camera-control');
+  createToggleControl(cameraCtrl, "videocam", onVideoCaptureToggle);
 
-  const deviceScreen = document.getElementById('device-screen');
   const deviceAudio = document.getElementById('device-audio');
+  const deviceDisplays = document.getElementById('device-displays');
   const statusMessage = document.getElementById('status-message');
 
   let connectionAttemptDuration = 0;
@@ -47,23 +49,6 @@
     }
   }, intervalMs);
 
-  deviceScreen.addEventListener('loadeddata', (evt) => {
-    clearInterval(animateDeviceStatusMessage);
-    statusMessage.textContent = 'Awaiting adb connection...';
-    resizeDeviceView();
-    deviceScreen.style.visibility = 'visible';
-    // Enable the buttons after the screen is visible.
-    for (const [_, button] of Object.entries(buttons)) {
-      if (!button.adb) {
-        button.button.disabled = false;
-      }
-    }
-    // Start the adb connection if it is not already started.
-    initializeAdb();
-  });
-
-  let videoStream;
-  let display_label;
   let buttons = {};
   let mouseIsDown = false;
   let deviceConnection;
@@ -106,7 +91,7 @@
   }
 
   let currentRotation = 0;
-  let currentDisplayDetails;
+  let currentDisplayDescriptions;
   function onControlMessage(message) {
     let message_data = JSON.parse(message.data);
     console.log(message_data)
@@ -120,46 +105,234 @@
     if (message_data.event == 'VIRTUAL_DEVICE_SCREEN_CHANGED') {
       if (metadata.rotation != currentRotation) {
         // Animate the screen rotation.
-        deviceScreen.style.transition = 'transform 1s';
-      } else {
-        // Don't animate screen resizes, since these appear as odd sliding
-        // animations if the screen is rotated due to the translateY.
-        deviceScreen.style.transition = '';
+        const targetRotation = metadata.rotation == 0 ? 0 : -90;
+
+        $(deviceDisplays).animate(
+          {
+            textIndent: targetRotation,
+          },
+          {
+            duration: 1000,
+            step: function(now, tween) {
+              resizeDeviceDisplays();
+            },
+          }
+        );
       }
 
       currentRotation = metadata.rotation;
-      updateDeviceDisplayDetails({
-        dpi: metadata.dpi,
-        x_res: metadata.width,
-        y_res: metadata.height
+    }
+    if (message_data.event == 'VIRTUAL_DEVICE_CAPTURE_IMAGE') {
+      if (deviceConnection.cameraEnabled) {
+        takePhoto();
+      }
+    }
+    if (message_data.event == 'VIRTUAL_DEVICE_DISPLAY_POWER_MODE_CHANGED') {
+      updateDisplayVisibility(metadata.display, metadata.mode);
+    }
+  }
+
+  function updateDisplayVisibility(displayId, powerMode) {
+    const display = document.getElementById('display_' + displayId).parentElement;
+    if (display == null) {
+      console.error('Unknown display id: ' + displayId);
+      return;
+    }
+    switch (powerMode) {
+      case 'On':
+        display.style.visibility = 'visible';
+        break;
+      case 'Off':
+        display.style.visibility = 'hidden';
+        break;
+      default:
+        console.error('Display ' + displayId + ' has unknown display power mode: ' + powerMode);
+    }
+  }
+
+  function getTransformRotation(element) {
+    if (!element.style.textIndent) {
+      return 0;
+    }
+    // Remove 'px' and convert to float.
+    return parseFloat(element.style.textIndent.slice(0, -2));
+  }
+
+  let anyDeviceDisplayLoaded = false;
+  function onDeviceDisplayLoaded() {
+    if (anyDeviceDisplayLoaded) {
+      return;
+    }
+    anyDeviceDisplayLoaded = true;
+
+    clearInterval(animateDeviceStatusMessage);
+    statusMessage.textContent = 'Awaiting bootup and adb connection. Please wait...';
+    resizeDeviceDisplays();
+
+    let deviceDisplayList =
+      document.getElementsByClassName("device-display");
+    for (const deviceDisplay of deviceDisplayList) {
+      deviceDisplay.style.visibility = 'visible';
+    }
+
+    // Enable the buttons after the screen is visible.
+    for (const [_, button] of Object.entries(buttons)) {
+      if (!button.adb) {
+        button.button.disabled = false;
+      }
+    }
+    // Start the adb connection if it is not already started.
+    initializeAdb();
+  }
+
+  // Creates a <video> element and a <div> container element for each display.
+  // The extra <div> container elements are used to maintain the width and
+  // height of the device as the CSS 'transform' property used on the <video>
+  // element for rotating the device only affects the visuals of the element
+  // and not its layout.
+  function createDeviceDisplays(devConn) {
+    for (const deviceDisplayDescription of currentDisplayDescriptions) {
+      let deviceDisplay = document.createElement("div");
+      deviceDisplay.classList.add("device-display");
+      // Start the screen as hidden. Only show when data is ready.
+      deviceDisplay.style.visibility = 'hidden';
+
+      let deviceDisplayInfo = document.createElement("div");
+      deviceDisplayInfo.classList.add("device-display-info");
+      deviceDisplayInfo.id = deviceDisplayDescription.stream_id + '_info';
+      deviceDisplay.appendChild(deviceDisplayInfo);
+
+      let deviceDisplayVideo = document.createElement("video");
+      deviceDisplayVideo.autoplay = true;
+      deviceDisplayVideo.id = deviceDisplayDescription.stream_id;
+      deviceDisplayVideo.classList.add("device-display-video");
+      deviceDisplayVideo.addEventListener('loadeddata', (evt) => {
+        onDeviceDisplayLoaded();
       });
+      deviceDisplay.appendChild(deviceDisplayVideo);
 
-      resizeDeviceView();
+      deviceDisplays.appendChild(deviceDisplay);
+
+      let stream_id = deviceDisplayDescription.stream_id;
+      devConn.getStream(stream_id).then(stream => {
+        deviceDisplayVideo.srcObject = stream;
+      }).catch(e => console.error('Unable to get display stream: ', e));
     }
   }
 
-  const screensDiv = document.getElementById('screens');
-  function resizeDeviceView() {
+  function takePhoto() {
+    const imageCapture = deviceConnection.imageCapture;
+    if (imageCapture) {
+      const photoSettings = {
+        imageWidth: deviceConnection.cameraWidth,
+        imageHeight: deviceConnection.cameraHeight
+      }
+      imageCapture.takePhoto(photoSettings)
+        .then(blob => blob.arrayBuffer())
+        .then(buffer => deviceConnection.sendOrQueueCameraData(buffer))
+        .catch(error => console.log(error));
+    }
+  }
+
+  function resizeDeviceDisplays() {
+    // Padding between displays.
+    const deviceDisplayWidthPadding = 10;
+    // Padding for the display info above each display video.
+    const deviceDisplayHeightPadding = 38;
+
+    let deviceDisplayList =
+      document.getElementsByClassName("device-display");
+    let deviceDisplayVideoList =
+      document.getElementsByClassName("device-display-video");
+    let deviceDisplayInfoList =
+      document.getElementsByClassName("device-display-info");
+
+    const rotationDegrees = getTransformRotation(deviceDisplays);
+    const rotationRadians = rotationDegrees * Math.PI / 180;
+
     // Auto-scale the screen based on window size.
-    // Max window width of 70%, allowing space for the control panel.
-    let ww = screensDiv.offsetWidth * 0.7;
-    let wh = screensDiv.offsetHeight;
-    let vw = currentDisplayDetails.x_res;
-    let vh = currentDisplayDetails.y_res;
-    let scaling = vw * wh > vh * ww ? ww / vw : wh / vh;
-    if (currentRotation == 0) {
-      deviceScreen.style.transform = null;
-      deviceScreen.style.width = vw * scaling;
-      deviceScreen.style.height = vh * scaling;
-    } else if (currentRotation == 1) {
-      deviceScreen.style.transform =
-          `rotateZ(-90deg) translateY(-${vh * scaling}px)`;
-      // When rotated, w and h are swapped.
-      deviceScreen.style.width = vh * scaling;
-      deviceScreen.style.height = vw * scaling;
+    let availableWidth = deviceDisplays.clientWidth;
+    let availableHeight = deviceDisplays.clientHeight - deviceDisplayHeightPadding;
+
+    // Reserve space for padding between the displays.
+    availableWidth = availableWidth -
+      (currentDisplayDescriptions.length * deviceDisplayWidthPadding);
+
+    // Loop once over all of the displays to compute the total space needed.
+    let neededWidth = 0;
+    let neededHeight = 0;
+    for (let i = 0; i < deviceDisplayList.length; i++) {
+      let deviceDisplayDescription = currentDisplayDescriptions[i];
+      let deviceDisplayVideo = deviceDisplayVideoList[i];
+
+      const originalDisplayWidth = deviceDisplayDescription.x_res;
+      const originalDisplayHeight = deviceDisplayDescription.y_res;
+
+      const neededBoundingBoxWidth =
+        Math.abs(Math.cos(rotationRadians) * originalDisplayWidth) +
+        Math.abs(Math.sin(rotationRadians) * originalDisplayHeight);
+      const neededBoundingBoxHeight =
+        Math.abs(Math.sin(rotationRadians) * originalDisplayWidth) +
+        Math.abs(Math.cos(rotationRadians) * originalDisplayHeight);
+
+      neededWidth = neededWidth + neededBoundingBoxWidth;
+      neededHeight = Math.max(neededHeight, neededBoundingBoxHeight);
+    }
+
+    const scaling = Math.min(availableWidth / neededWidth,
+                             availableHeight / neededHeight);
+
+    // Loop again over all of the displays to set the sizes and positions.
+    let deviceDisplayLeftOffset = 0;
+    for (let i = 0; i < deviceDisplayList.length; i++) {
+      let deviceDisplay = deviceDisplayList[i];
+      let deviceDisplayVideo = deviceDisplayVideoList[i];
+      let deviceDisplayInfo = deviceDisplayInfoList[i];
+      let deviceDisplayDescription = currentDisplayDescriptions[i];
+
+      let rotated = currentRotation == 1 ? ' (Rotated)' : '';
+      deviceDisplayInfo.textContent = `Display ${i} - ` +
+          `${deviceDisplayDescription.x_res}x` +
+          `${deviceDisplayDescription.y_res} ` +
+          `(${deviceDisplayDescription.dpi} DPI)${rotated}`;
+
+      const originalDisplayWidth = deviceDisplayDescription.x_res;
+      const originalDisplayHeight = deviceDisplayDescription.y_res;
+
+      const scaledDisplayWidth = originalDisplayWidth * scaling;
+      const scaledDisplayHeight = originalDisplayHeight * scaling;
+
+      const neededBoundingBoxWidth =
+        Math.abs(Math.cos(rotationRadians) * originalDisplayWidth) +
+        Math.abs(Math.sin(rotationRadians) * originalDisplayHeight);
+      const neededBoundingBoxHeight =
+        Math.abs(Math.sin(rotationRadians) * originalDisplayWidth) +
+        Math.abs(Math.cos(rotationRadians) * originalDisplayHeight);
+
+      const scaledBoundingBoxWidth = neededBoundingBoxWidth * scaling;
+      const scaledBoundingBoxHeight = neededBoundingBoxHeight * scaling;
+
+      const offsetX = (scaledBoundingBoxWidth - scaledDisplayWidth) / 2;
+      const offsetY = (scaledBoundingBoxHeight - scaledDisplayHeight) / 2;
+
+      deviceDisplayVideo.style.width = scaledDisplayWidth;
+      deviceDisplayVideo.style.height = scaledDisplayHeight;
+      deviceDisplayVideo.style.transform =
+        `translateX(${offsetX}px) ` +
+        `translateY(${offsetY}px) ` +
+        `rotateZ(${rotationDegrees}deg) `;
+
+      deviceDisplay.style.left = `${deviceDisplayLeftOffset}px`;
+      deviceDisplay.style.width = scaledBoundingBoxWidth;
+      deviceDisplay.style.height = scaledBoundingBoxHeight;
+
+      deviceDisplayLeftOffset =
+        deviceDisplayLeftOffset +
+        deviceDisplayWidthPadding +
+        scaledBoundingBoxWidth;
     }
   }
-  window.onresize = resizeDeviceView;
+  window.onresize = resizeDeviceDisplays;
 
   function createControlPanelButton(command, title, icon_name,
       listener=onControlPanelButton,
@@ -268,7 +441,7 @@
     statusMessage.textContent = 'No connection to the guest device. ' +
         'Please ensure the WebRTC process on the host machine is active.';
     statusMessage.style.visibility = 'visible';
-    deviceScreen.style.display = 'none';
+    deviceDisplays.style.display = 'none';
     for (const [_, button] of Object.entries(buttons)) {
       button.button.disabled = true;
     }
@@ -278,15 +451,12 @@
     .then(webrtcModule => webrtcModule.Connect(device_id, options))
     .then(devConn => {
       deviceConnection = devConn;
-      // TODO(b/143667633): get multiple display configuration from the
-      // description object
+
       console.log(deviceConnection.description);
-      let stream_id = devConn.description.displays[0].stream_id;
-      devConn.getStream(stream_id).then(stream => {
-        videoStream = stream;
-        display_label = stream_id;
-        deviceScreen.srcObject = videoStream;
-      }).catch(e => console.error('Unable to get display stream: ', e));
+
+      currentDisplayDescriptions = devConn.description.displays;
+
+      createDeviceDisplays(devConn);
       for (const audio_desc of devConn.description.audio_streams) {
         let stream_id = audio_desc.stream_id;
         devConn.getStream(stream_id).then(stream => {
@@ -295,7 +465,6 @@
       }
       startMouseTracking();  // TODO stopMouseTracking() when disconnected
       updateDeviceHardwareDetails(deviceConnection.description.hardware);
-      updateDeviceDisplayDetails(deviceConnection.description.displays[0]);
       if (deviceConnection.description.custom_control_panel_buttons.length > 0) {
         document.getElementById('control-panel-custom-buttons').style.display = 'flex';
         for (const button of deviceConnection.description.custom_control_panel_buttons) {
@@ -326,8 +495,6 @@
         }
       }
       deviceConnection.onControlMessage(msg => onControlMessage(msg));
-      // Start the screen as hidden. Only show when data is ready.
-      deviceScreen.style.visibility = 'hidden';
       // Show the error message and disable buttons when the WebRTC connection fails.
       deviceConnection.onConnectionStateChange(state => {
         if (state == 'disconnected' || state == 'failed') {
@@ -343,13 +510,11 @@
   });
 
   let hardwareDetailsText = '';
-  let displayDetailsText = '';
   let deviceStateDetailsText = '';
   function updateDeviceDetailsText() {
     document.getElementById('device-details-hardware').textContent = [
       hardwareDetailsText,
       deviceStateDetailsText,
-      displayDetailsText,
     ].filter(e => e /*remove empty*/).join('\n');
   }
   function updateDeviceHardwareDetails(hardware) {
@@ -362,15 +527,6 @@
     hardwareDetailsText = hardwareDetailsTextLines.join('\n');
     updateDeviceDetailsText();
   }
-  function updateDeviceDisplayDetails(display) {
-    currentDisplayDetails = display;
-    let dpi = display.dpi;
-    let x_res = display.x_res;
-    let y_res = display.y_res;
-    let rotated = currentRotation == 1 ? ' (Rotated)' : '';
-    displayDetailsText = `Display - ${x_res}x${y_res} (${dpi}DPI)${rotated}`;
-    updateDeviceDetailsText();
-  }
   function updateDeviceStateDetails() {
     let deviceStateDetailsTextLines = [];
     if (deviceStateLidSwitchOpen != null) {
@@ -396,6 +552,10 @@
     deviceConnection.useMic(enabled);
   }
 
+  function onVideoCaptureToggle(enabled) {
+    deviceConnection.useVideo(enabled);
+  }
+
   function cmdConsole(consoleViewName, consoleInputName) {
     let consoleView = document.getElementById(consoleViewName);
 
@@ -523,34 +683,48 @@
   }
 
   function startMouseTracking() {
+    let deviceDisplayList = document.getElementsByClassName("device-display");
     if (window.PointerEvent) {
-      deviceScreen.addEventListener('pointerdown', onStartDrag);
-      deviceScreen.addEventListener('pointermove', onContinueDrag);
-      deviceScreen.addEventListener('pointerup', onEndDrag);
+      for (const deviceDisplay of deviceDisplayList) {
+        deviceDisplay.addEventListener('pointerdown', onStartDrag);
+        deviceDisplay.addEventListener('pointermove', onContinueDrag);
+        deviceDisplay.addEventListener('pointerup', onEndDrag);
+      }
     } else if (window.TouchEvent) {
-      deviceScreen.addEventListener('touchstart', onStartDrag);
-      deviceScreen.addEventListener('touchmove', onContinueDrag);
-      deviceScreen.addEventListener('touchend', onEndDrag);
+      for (const deviceDisplay of deviceDisplayList) {
+        deviceDisplay.addEventListener('touchstart', onStartDrag);
+        deviceDisplay.addEventListener('touchmove', onContinueDrag);
+        deviceDisplay.addEventListener('touchend', onEndDrag);
+      }
     } else if (window.MouseEvent) {
-      deviceScreen.addEventListener('mousedown', onStartDrag);
-      deviceScreen.addEventListener('mousemove', onContinueDrag);
-      deviceScreen.addEventListener('mouseup', onEndDrag);
+      for (const deviceDisplay of deviceDisplayList) {
+        deviceDisplay.addEventListener('mousedown', onStartDrag);
+        deviceDisplay.addEventListener('mousemove', onContinueDrag);
+        deviceDisplay.addEventListener('mouseup', onEndDrag);
+      }
     }
   }
 
   function stopMouseTracking() {
+    let deviceDisplayList = document.getElementsByClassName("device-display");
     if (window.PointerEvent) {
-      deviceScreen.removeEventListener('pointerdown', onStartDrag);
-      deviceScreen.removeEventListener('pointermove', onContinueDrag);
-      deviceScreen.removeEventListener('pointerup', onEndDrag);
+      for (const deviceDisplay of deviceDisplayList) {
+        deviceDisplay.removeEventListener('pointerdown', onStartDrag);
+        deviceDisplay.removeEventListener('pointermove', onContinueDrag);
+        deviceDisplay.removeEventListener('pointerup', onEndDrag);
+      }
     } else if (window.TouchEvent) {
-      deviceScreen.removeEventListener('touchstart', onStartDrag);
-      deviceScreen.removeEventListener('touchmove', onContinueDrag);
-      deviceScreen.removeEventListener('touchend', onEndDrag);
+      for (const deviceDisplay of deviceDisplayList) {
+        deviceDisplay.removeEventListener('touchstart', onStartDrag);
+        deviceDisplay.removeEventListener('touchmove', onContinueDrag);
+        deviceDisplay.removeEventListener('touchend', onEndDrag);
+      }
     } else if (window.MouseEvent) {
-      deviceScreen.removeEventListener('mousedown', onStartDrag);
-      deviceScreen.removeEventListener('mousemove', onContinueDrag);
-      deviceScreen.removeEventListener('mouseup', onEndDrag);
+      for (const deviceDisplay of deviceDisplayList) {
+        deviceDisplay.removeEventListener('mousedown', onStartDrag);
+        deviceDisplay.removeEventListener('mousemove', onContinueDrag);
+        deviceDisplay.removeEventListener('mouseup', onEndDrag);
+      }
     }
   }
 
@@ -596,14 +770,17 @@
     console.assert(deviceConnection, 'Can\'t send mouse update without device');
     var eventType = e.type.substring(0, 5);
 
+    // The <video> element:
+    const deviceDisplay = e.target;
+
     // Before the first video frame arrives there is no way to know width and
     // height of the device's screen, so turn every click into a click at 0x0.
     // A click at that position is not more dangerous than anywhere else since
     // the user is clicking blind anyways.
-    const videoWidth = deviceScreen.videoWidth? deviceScreen.videoWidth: 1;
-    const videoHeight = deviceScreen.videoHeight? deviceScreen.videoHeight: 1;
-    const elementWidth = deviceScreen.offsetWidth? deviceScreen.offsetWidth: 1;
-    const elementHeight = deviceScreen.offsetHeight? deviceScreen.offsetHeight: 1;
+    const videoWidth = deviceDisplay.videoWidth? deviceDisplay.videoWidth: 1;
+    const videoHeight = deviceDisplay.videoHeight? deviceDisplay.videoHeight: 1;
+    const elementWidth = deviceDisplay.offsetWidth? deviceDisplay.offsetWidth: 1;
+    const elementHeight = deviceDisplay.offsetHeight? deviceDisplay.offsetHeight: 1;
 
     // vh*ew > eh*vw? then scale h instead of w
     const scaleHeight = videoHeight * elementWidth > videoWidth * elementHeight;
@@ -732,6 +909,8 @@
     // NOTE: Rotation is handled automatically because the CSS rotation through
     // transforms also rotates the coordinates of events on the object.
 
+    const display_label = deviceDisplay.id;
+
     deviceConnection.sendMultiTouch(
     {idArr, xArr, yArr, down, slotArr, display_label});
   }
diff --git a/host/frontend/webrtc_operator/assets/js/cf_webrtc.js b/host/frontend/webrtc_operator/assets/js/cf_webrtc.js
index c9d8213..c83ee38 100644
--- a/host/frontend/webrtc_operator/assets/js/cf_webrtc.js
+++ b/host/frontend/webrtc_operator/assets/js/cf_webrtc.js
@@ -82,12 +82,22 @@
 }
 
 class DeviceConnection {
-  constructor(pc, control, audio_stream) {
+  constructor(pc, control, media_stream) {
     this._pc = pc;
     this._control = control;
-    this._audio_stream = audio_stream;
+    this._media_stream = media_stream;
     // Disable the microphone by default
     this.useMic(false);
+    this.useVideo(false);
+    this._cameraDataChannel = pc.createDataChannel('camera-data-channel');
+    this._cameraDataChannel.binaryType = 'arraybuffer'
+    this._cameraInputQueue = new Array();
+    var self = this;
+    this._cameraDataChannel.onbufferedamountlow = () => {
+      if (self._cameraInputQueue.length > 0) {
+        self.sendCameraData(self._cameraInputQueue.shift());
+      }
+    }
     this._inputChannel = createDataChannel(pc, 'input-channel');
     this._adbChannel = createDataChannel(pc, 'adb-channel', (msg) => {
       if (this._onAdbMessage) {
@@ -110,6 +120,7 @@
         console.error('Received unexpected Bluetooth message');
       }
     });
+    this.sendCameraResolution();
     this._streams = {};
     this._streamPromiseResolvers = {};
 
@@ -135,6 +146,28 @@
     return this._description;
   }
 
+  get imageCapture() {
+    if (this._media_stream) {
+      const track = this._media_stream.getVideoTracks()[0]
+      return new ImageCapture(track);
+    }
+    return undefined;
+  }
+
+  get cameraWidth() {
+    return this._x_res;
+  }
+
+  get cameraHeight() {
+    return this._y_res;
+  }
+
+  get cameraEnabled() {
+    if (this._media_stream) {
+      return this._media_stream.getVideoTracks().some(track => track.enabled);
+    }
+  }
+
   getStream(stream_id) {
     return new Promise((resolve, reject) => {
       if (this._streams[stream_id]) {
@@ -200,11 +233,53 @@
   }
 
   useMic(in_use) {
-    if (this._audio_stream) {
-      this._audio_stream.getTracks().forEach(track => track.enabled = in_use);
+    if (this._media_stream) {
+      this._media_stream.getAudioTracks().forEach(track => track.enabled = in_use);
     }
   }
 
+  useVideo(in_use) {
+    if (this._media_stream) {
+      this._media_stream.getVideoTracks().forEach(track => track.enabled = in_use);
+    }
+  }
+
+  sendCameraResolution() {
+    if (this._media_stream) {
+      const cameraTracks = this._media_stream.getVideoTracks();
+      if (cameraTracks.length > 0) {
+        const settings = cameraTracks[0].getSettings();
+        this._x_res = settings.width;
+        this._y_res = settings.height;
+        this.sendControlMessage(JSON.stringify({
+          command: 'camera_settings',
+          width: settings.width,
+          height: settings.height,
+          frame_rate: settings.frameRate,
+          facing: settings.facingMode
+        }));
+      }
+    }
+  }
+
+  sendOrQueueCameraData(data) {
+    if (this._cameraDataChannel.bufferedAmount > 0 || this._cameraInputQueue.length > 0) {
+      this._cameraInputQueue.push(data);
+    } else {
+      this.sendCameraData(data);
+    }
+  }
+
+  sendCameraData(data) {
+    const MAX_SIZE = 65535;
+    const END_MARKER = 'EOF';
+    for (let i = 0; i < data.byteLength; i += MAX_SIZE) {
+      // range is clamped to the valid index range
+      this._cameraDataChannel.send(data.slice(i, i + MAX_SIZE));
+    }
+    this._cameraDataChannel.send(END_MARKER);
+  }
+
   // Provide a callback to receive control-related comms from the device
   onControlMessage(cb) {
     this._onControlMessage = cb;
@@ -404,21 +479,20 @@
   }
   let pc = createPeerConnection(infraConfig, control);
 
-  let audioStream;
+  let mediaStream;
   try {
-    audioStream =
-        await navigator.mediaDevices.getUserMedia({video: false, audio: true});
-    const audioTracks = audioStream.getAudioTracks();
-    if (audioTracks.length > 0) {
-      console.log(`Using Audio device: ${audioTracks[0].label}, with ${
-        audioTracks.length} tracks`);
-      audioTracks.forEach(track => pc.addTrack(track, audioStream));
-    }
+    mediaStream =
+        await navigator.mediaDevices.getUserMedia({video: true, audio: true});
+    const tracks = mediaStream.getTracks();
+    tracks.forEach(track => {
+      console.log(`Using ${track.kind} device: ${track.label}`);
+      pc.addTrack(track, mediaStream);
+    });
   } catch (e) {
     console.error("Failed to open audio device: ", e);
   }
 
-  let deviceConnection = new DeviceConnection(pc, control, audioStream);
+  let deviceConnection = new DeviceConnection(pc, control, mediaStream);
   deviceConnection.description = deviceInfo;
   async function acceptOfferAndReplyAnswer(offer) {
     try {
diff --git a/host/frontend/webrtc_operator/assets/style.css b/host/frontend/webrtc_operator/assets/style.css
index 5d59b34..a594e99 100644
--- a/host/frontend/webrtc_operator/assets/style.css
+++ b/host/frontend/webrtc_operator/assets/style.css
@@ -17,6 +17,8 @@
 body {
   background-color:black;
   margin: 0;
+  touch-action: none;
+  overscroll-behavior: none;
 }
 
 #device-selector {
@@ -85,13 +87,14 @@
 
 /* Control panel buttons and device screen(s). */
 
-#controls-and-screens {
+#controls-and-displays {
   height: calc(100% - 84px);
 
   /* Items inside this use a row Flexbox.*/
   display: flex;
 }
-#controls-and-screens div {
+
+#controls-and-displays > div {
   margin-left: 5px;
   margin-right: 5px;
 }
@@ -163,15 +166,41 @@
   background-color: #5f6368; /* Google grey 700 */
 }
 
-#screens {
+#device-displays {
   /* Take up the remaining width of the window.*/
   flex-grow: 1;
   /* Don't grow taller than the window.*/
   max-height: 100vh;
+  /* Allows child elements to be positioned relative to this element. */
+  position: relative;
 }
 
-#device-screen {
+/*
+ * Container <div> used to wrap each display's <video> element which is used for
+ * maintaining each display's width and height while the display is potentially
+ * rotating.
+ */
+.device-display {
+  /* Prevents #device-displays from using this element when computing flex size. */
+  position: absolute;
+}
+
+/* Container <div> to show info about the individual display. */
+.device-display-info {
+  color: white;
+  /* dark green */
+  background-color: #007000;
+  font-family: 'Open Sans', sans-serif;
+  text-indent: 0px;
+  border-radius: 10px;
+  padding: 10px;
+  margin-bottom: 10px;
+}
+
+/* The actual <video> element for each display. */
+.device-display-video {
+  position: absolute;
+  left:  0px;
   touch-action: none;
-  transform-origin: top right;
   object-fit: cover;
 }
diff --git a/host/frontend/webrtc_operator/server.cpp b/host/frontend/webrtc_operator/server.cpp
index 565676a..c1f08f7 100644
--- a/host/frontend/webrtc_operator/server.cpp
+++ b/host/frontend/webrtc_operator/server.cpp
@@ -31,7 +31,9 @@
 DEFINE_bool(use_secure_http, true, "Whether to use HTTPS or HTTP.");
 DEFINE_string(assets_dir, "webrtc",
               "Directory with location of webpage assets.");
-DEFINE_string(certs_dir, "webrtc/certs", "Directory to certificates.");
+DEFINE_string(certs_dir, "webrtc/certs",
+              "Directory to certificates. It must contain a server.crt file, a "
+              "server.key file and (optionally) a CA.crt file.");
 DEFINE_string(stun_server, "stun.l.google.com:19302",
               "host:port of STUN server to use for public address resolution");
 
diff --git a/host/libs/audio_connector/commands.cpp b/host/libs/audio_connector/commands.cpp
index 42536d5..b77c726 100644
--- a/host/libs/audio_connector/commands.cpp
+++ b/host/libs/audio_connector/commands.cpp
@@ -15,6 +15,8 @@
 
 #include "host/libs/audio_connector/commands.h"
 
+#include <algorithm>
+
 #include <android-base/logging.h>
 
 #include "host/libs/audio_connector/shm_layout.h"
@@ -27,6 +29,50 @@
       << " went out of scope without reply";
 }
 
+JackInfoCommand::JackInfoCommand(uint32_t start_id, size_t count,
+                                 virtio_snd_jack_info* jack_info)
+    : InfoCommand(AudioCommandType::VIRTIO_SND_R_CHMAP_INFO, start_id, count,
+                  jack_info) {}
+
+void JackInfoCommand::Reply(AudioStatus status,
+                            const std::vector<virtio_snd_jack_info>& reply) {
+  MarkReplied(status);
+  if (status != AudioStatus::VIRTIO_SND_S_OK) {
+    return;
+  }
+  CHECK(reply.size() == count())
+      << "Returned unmatching info count: " << reply.size() << " vs "
+      << count();
+  for (int i = 0; i < reply.size(); ++i) {
+    info_reply()[i] = reply[i];
+  }
+}
+
+ChmapInfoCommand::ChmapInfoCommand(uint32_t start_id, size_t count,
+                                   virtio_snd_chmap_info* chmap_info)
+    : InfoCommand(AudioCommandType::VIRTIO_SND_R_CHMAP_INFO, start_id, count,
+                  chmap_info) {}
+
+void ChmapInfoCommand::Reply(AudioStatus status,
+                             const std::vector<virtio_snd_chmap_info>& reply) {
+  MarkReplied(status);
+  if (status != AudioStatus::VIRTIO_SND_S_OK) {
+    return;
+  }
+  CHECK(reply.size() == count())
+      << "Returned unmatching info count: " << reply.size() << " vs "
+      << count();
+  for (int i = 0; i < reply.size(); ++i) {
+    info_reply()[i].hdr.hda_fn_nid = Le32(reply[i].hdr.hda_fn_nid);
+    info_reply()[i].direction = reply[i].direction;
+    auto channels = std::min(VIRTIO_SND_CHMAP_MAX_SIZE, reply[i].channels);
+    info_reply()[i].channels = channels;
+    for (int j = 0; j < channels; ++j) {
+	    info_reply()[i].positions[j] = reply[i].positions[j];
+    }
+  }
+}
+
 StreamInfoCommand::StreamInfoCommand(uint32_t start_id, size_t count,
                                      virtio_snd_pcm_info* pcm_info)
     : InfoCommand(AudioCommandType::VIRTIO_SND_R_PCM_INFO, start_id, count,
diff --git a/host/libs/audio_connector/commands.h b/host/libs/audio_connector/commands.h
index 849dc12..1ae2e6b 100644
--- a/host/libs/audio_connector/commands.h
+++ b/host/libs/audio_connector/commands.h
@@ -60,6 +60,24 @@
   R* info_reply_;
 };
 
+class ChmapInfoCommand : public InfoCommand<virtio_snd_chmap_info> {
+ public:
+  ChmapInfoCommand(uint32_t start_id, size_t count,
+                   virtio_snd_chmap_info* chmap_info);
+
+  void Reply(AudioStatus status,
+             const std::vector<virtio_snd_chmap_info>& reply);
+};
+
+class JackInfoCommand : public InfoCommand<virtio_snd_jack_info> {
+ public:
+  JackInfoCommand(uint32_t start_id, size_t count,
+                   virtio_snd_jack_info* jack_info);
+
+  void Reply(AudioStatus status,
+             const std::vector<virtio_snd_jack_info>& reply);
+};
+
 class StreamInfoCommand : public InfoCommand<virtio_snd_pcm_info> {
  public:
   StreamInfoCommand(uint32_t start_id, size_t count,
diff --git a/host/libs/audio_connector/server.cpp b/host/libs/audio_connector/server.cpp
index b925cdd..c153df9 100644
--- a/host/libs/audio_connector/server.cpp
+++ b/host/libs/audio_connector/server.cpp
@@ -21,6 +21,7 @@
 #include <unistd.h>
 
 #include <utility>
+#include <vector>
 
 #include <android-base/logging.h>
 
@@ -244,8 +245,38 @@
       executor.StopStream(cmd);
       return CmdReply(cmd.status());
     }
-    case AudioCommandType::VIRTIO_SND_R_CHMAP_INFO:
-    case AudioCommandType::VIRTIO_SND_R_JACK_INFO:
+    case AudioCommandType::VIRTIO_SND_R_CHMAP_INFO: {
+      if (recv_size < sizeof(virtio_snd_query_info)) {
+        LOG(ERROR) << "Received QUERY_INFO message is too small: " << recv_size;
+        return false;
+      }
+      auto query_info = reinterpret_cast<const virtio_snd_query_info*>(cmd_hdr);
+      auto info_count = query_info->count.as_uint32_t();
+      auto start_id = query_info->start_id.as_uint32_t();
+      std::unique_ptr<virtio_snd_chmap_info[]> reply(
+          new virtio_snd_chmap_info[info_count]);
+      ChmapInfoCommand cmd(start_id, info_count, reply.get());
+
+      executor.ChmapsInfo(cmd);
+      return CmdReply(cmd.status(), reply.get(),
+                      info_count * sizeof(reply[0]));
+    }
+    case AudioCommandType::VIRTIO_SND_R_JACK_INFO: {
+      if (recv_size < sizeof(virtio_snd_query_info)) {
+        LOG(ERROR) << "Received QUERY_INFO message is too small: " << recv_size;
+        return false;
+      }
+      auto query_info = reinterpret_cast<const virtio_snd_query_info*>(cmd_hdr);
+      auto info_count = query_info->count.as_uint32_t();
+      auto start_id = query_info->start_id.as_uint32_t();
+      std::unique_ptr<virtio_snd_jack_info[]> reply(
+          new virtio_snd_jack_info[info_count]);
+      JackInfoCommand cmd(start_id, info_count, reply.get());
+
+      executor.JacksInfo(cmd);
+      return CmdReply(cmd.status(), reply.get(),
+                      info_count * sizeof(reply[0]));
+    }
     case AudioCommandType::VIRTIO_SND_R_JACK_REMAP:
       LOG(ERROR) << "Unsupported command type: " << cmd_hdr->code.as_uint32_t();
       return CmdReply(AudioStatus::VIRTIO_SND_S_NOT_SUPP);
@@ -302,21 +333,15 @@
   virtio_snd_hdr vio_status = {
       .code = Le32(static_cast<uint32_t>(status)),
   };
-  auto status_sent = control_socket_->Send(&vio_status, sizeof(vio_status), 0);
-  if (status_sent < sizeof(vio_status)) {
+  std::vector<uint8_t> buffer(sizeof(vio_status) + size, 0);
+  std::memcpy(buffer.data(), &vio_status, sizeof(vio_status));
+  std::memcpy(buffer.data() + sizeof(vio_status), data, size);
+  auto status_sent = control_socket_->Send(buffer.data(), buffer.size(), 0);
+  if (status_sent < sizeof(vio_status) + size) {
     LOG(ERROR) << "Failed to send entire command status: "
                << control_socket_->StrError();
     return false;
   }
-  if (status != AudioStatus::VIRTIO_SND_S_OK || size == 0) {
-    return true;
-  }
-  auto payload_sent = control_socket_->Send(data, size, 0);
-  if (payload_sent < size) {
-    LOG(ERROR) << "Failed to send entire command response payload: "
-               << control_socket_->StrError();
-    return false;
-  }
   return true;
 }
 
diff --git a/host/libs/audio_connector/server.h b/host/libs/audio_connector/server.h
index 4ba2e7c..6fa14d9 100644
--- a/host/libs/audio_connector/server.h
+++ b/host/libs/audio_connector/server.h
@@ -41,6 +41,8 @@
   virtual void ReleaseStream(StreamControlCommand& cmd) = 0;
   virtual void StartStream(StreamControlCommand& cmd) = 0;
   virtual void StopStream(StreamControlCommand& cmd) = 0;
+  virtual void ChmapsInfo(ChmapInfoCommand& cmd) = 0;
+  virtual void JacksInfo(JackInfoCommand& cmd) = 0;
 
   // Implementations must call buffer.SendStatus() before destroying the buffer
   // to notify the other side of the release of the buffer. Failure to do so
diff --git a/host/libs/audio_connector/shm_layout.h b/host/libs/audio_connector/shm_layout.h
index 9148cb8..24000e6 100644
--- a/host/libs/audio_connector/shm_layout.h
+++ b/host/libs/audio_connector/shm_layout.h
@@ -51,7 +51,7 @@
   NOT_SET = static_cast<uint32_t>(-1),
 };
 
-enum class AudioStreamDirection : uint32_t {
+enum class AudioStreamDirection : uint8_t {
   VIRTIO_SND_D_OUTPUT = 0,
   VIRTIO_SND_D_INPUT
 };
@@ -104,6 +104,47 @@
   VIRTIO_SND_PCM_RATE_384000
 };
 
+/* standard channel position definition */
+enum AudioChannelMap : uint8_t {
+  VIRTIO_SND_CHMAP_NONE = 0,  /* undefined */
+  VIRTIO_SND_CHMAP_NA,        /* silent */
+  VIRTIO_SND_CHMAP_MONO,      /* mono stream */
+  VIRTIO_SND_CHMAP_FL,        /* front left */
+  VIRTIO_SND_CHMAP_FR,        /* front right */
+  VIRTIO_SND_CHMAP_RL,        /* rear left */
+  VIRTIO_SND_CHMAP_RR,        /* rear right */
+  VIRTIO_SND_CHMAP_FC,        /* front center */
+  VIRTIO_SND_CHMAP_LFE,       /* low frequency (LFE) */
+  VIRTIO_SND_CHMAP_SL,        /* side left */
+  VIRTIO_SND_CHMAP_SR,        /* side right */
+  VIRTIO_SND_CHMAP_RC,        /* rear center */
+  VIRTIO_SND_CHMAP_FLC,       /* front left center */
+  VIRTIO_SND_CHMAP_FRC,       /* front right center */
+  VIRTIO_SND_CHMAP_RLC,       /* rear left center */
+  VIRTIO_SND_CHMAP_RRC,       /* rear right center */
+  VIRTIO_SND_CHMAP_FLW,       /* front left wide */
+  VIRTIO_SND_CHMAP_FRW,       /* front right wide */
+  VIRTIO_SND_CHMAP_FLH,       /* front left high */
+  VIRTIO_SND_CHMAP_FCH,       /* front center high */
+  VIRTIO_SND_CHMAP_FRH,       /* front right high */
+  VIRTIO_SND_CHMAP_TC,        /* top center */
+  VIRTIO_SND_CHMAP_TFL,       /* top front left */
+  VIRTIO_SND_CHMAP_TFR,       /* top front right */
+  VIRTIO_SND_CHMAP_TFC,       /* top front center */
+  VIRTIO_SND_CHMAP_TRL,       /* top rear left */
+  VIRTIO_SND_CHMAP_TRR,       /* top rear right */
+  VIRTIO_SND_CHMAP_TRC,       /* top rear center */
+  VIRTIO_SND_CHMAP_TFLC,      /* top front left center */
+  VIRTIO_SND_CHMAP_TFRC,      /* top front right center */
+  VIRTIO_SND_CHMAP_TSL,       /* top side left */
+  VIRTIO_SND_CHMAP_TSR,       /* top side right */
+  VIRTIO_SND_CHMAP_LLFE,      /* left LFE */
+  VIRTIO_SND_CHMAP_RLFE,      /* right LFE */
+  VIRTIO_SND_CHMAP_BC,        /* bottom center */
+  VIRTIO_SND_CHMAP_BLC,       /* bottom left center */
+  VIRTIO_SND_CHMAP_BRC        /* bottom right center */
+};
+
 struct virtio_snd_hdr {
   Le32 code;
 };
@@ -119,6 +160,29 @@
   Le32 hda_fn_nid;
 };
 
+/* supported jack features */
+enum AudioJackFeatures: uint8_t {
+  VIRTIO_SND_JACK_F_REMAP = 0
+};
+
+struct virtio_snd_jack_info {
+  struct virtio_snd_info hdr;
+  Le32 features; /* 1 << VIRTIO_SND_JACK_F_XXX */
+  Le32 hda_reg_defconf;
+  Le32 hda_reg_caps;
+  uint8_t connected;
+
+  uint8_t padding[7];
+};
+
+constexpr uint8_t VIRTIO_SND_CHMAP_MAX_SIZE = 18;
+struct virtio_snd_chmap_info {
+  struct virtio_snd_info hdr;
+  uint8_t direction;
+  uint8_t channels;
+  uint8_t positions[VIRTIO_SND_CHMAP_MAX_SIZE];
+};
+
 struct virtio_snd_pcm_info {
   struct virtio_snd_info hdr;
   Le32 features; /* 1 << VIRTIO_SND_PCM_F_XXX */
@@ -157,7 +221,7 @@
 };
 
 // Update this value when the msg layouts change
-const uint32_t VIOS_VERSION = 1;
+const uint32_t VIOS_VERSION = 2;
 
 struct VioSConfig {
   uint32_t version;
@@ -182,6 +246,8 @@
 #define ASSERT_VALID_MSG_TYPE(T, size) \
   static_assert(sizeof(T) == (size), #T " has the wrong size")
 ASSERT_VALID_MSG_TYPE(virtio_snd_query_info, 16);
+ASSERT_VALID_MSG_TYPE(virtio_snd_jack_info, 24);
+ASSERT_VALID_MSG_TYPE(virtio_snd_chmap_info, 24);
 ASSERT_VALID_MSG_TYPE(virtio_snd_pcm_info, 32);
 ASSERT_VALID_MSG_TYPE(virtio_snd_pcm_set_params, 24);
 ASSERT_VALID_MSG_TYPE(virtio_snd_pcm_hdr, 8);
@@ -189,4 +255,4 @@
 ASSERT_VALID_MSG_TYPE(IoStatusMsg, 16);
 #undef ASSERT_VALID_MSG_TYPE
 
-}  // namespace cuttlefish
\ No newline at end of file
+}  // namespace cuttlefish
diff --git a/host/libs/config/bootconfig_args.cpp b/host/libs/config/bootconfig_args.cpp
index c2bc1eb..1f926eb 100644
--- a/host/libs/config/bootconfig_args.cpp
+++ b/host/libs/config/bootconfig_args.cpp
@@ -90,7 +90,13 @@
 
   bootconfig_args.push_back(
       concat("androidboot.serialno=", instance.serial_number()));
-  bootconfig_args.push_back(concat("androidboot.lcd_density=", config.dpi()));
+
+  // TODO(b/131884992): update to specify multiple once supported.
+  const auto display_configs = config.display_configs();
+  CHECK_GE(display_configs.size(), 1);
+  bootconfig_args.push_back(
+      concat("androidboot.lcd_density=", display_configs[0].dpi));
+
   bootconfig_args.push_back(
       concat("androidboot.setupwizard_mode=", config.setupwizard_mode()));
   if (!config.guest_enforce_security()) {
@@ -143,6 +149,13 @@
                                      instance.frames_server_port()));
   }
 
+  if (instance.camera_server_port()) {
+    bootconfig_args.push_back(concat("androidboot.vsock_camera_port=",
+                                     instance.camera_server_port()));
+    bootconfig_args.push_back(
+        concat("androidboot.vsock_camera_cid=", instance.vsock_guest_cid()));
+  }
+
   if (config.enable_modem_simulator() &&
       instance.modem_simulator_ports() != "") {
     bootconfig_args.push_back(concat("androidboot.modem_simulator_ports=",
diff --git a/host/libs/config/cuttlefish_config.cpp b/host/libs/config/cuttlefish_config.cpp
index c04faae..b5160d0 100644
--- a/host/libs/config/cuttlefish_config.cpp
+++ b/host/libs/config/cuttlefish_config.cpp
@@ -123,13 +123,11 @@
   (*dictionary_)[kMemoryMb] = memory_mb;
 }
 
-static constexpr char kDpi[] = "dpi";
-int CuttlefishConfig::dpi() const { return (*dictionary_)[kDpi].asInt(); }
-void CuttlefishConfig::set_dpi(int dpi) { (*dictionary_)[kDpi] = dpi; }
-
 static constexpr char kDisplayConfigs[] = "display_configs";
 static constexpr char kXRes[] = "x_res";
 static constexpr char kYRes[] = "y_res";
+static constexpr char kDpi[] = "dpi";
+static constexpr char kRefreshRateHz[] = "refresh_rate_hz";
 std::vector<CuttlefishConfig::DisplayConfig>
 CuttlefishConfig::display_configs() const {
   std::vector<DisplayConfig> display_configs;
@@ -137,6 +135,9 @@
     DisplayConfig display_config = {};
     display_config.width = display_config_json[kXRes].asInt();
     display_config.height = display_config_json[kYRes].asInt();
+    display_config.dpi = display_config_json[kDpi].asInt();
+    display_config.refresh_rate_hz =
+        display_config_json[kRefreshRateHz].asInt();
     display_configs.emplace_back(std::move(display_config));
   }
   return display_configs;
@@ -149,20 +150,14 @@
     Json::Value display_config_json(Json::objectValue);
     display_config_json[kXRes] = display_configs.width;
     display_config_json[kYRes] = display_configs.height;
+    display_config_json[kDpi] = display_configs.dpi;
+    display_config_json[kRefreshRateHz] = display_configs.refresh_rate_hz;
     display_configs_json.append(display_config_json);
   }
 
   (*dictionary_)[kDisplayConfigs] = display_configs_json;
 }
 
-static constexpr char kRefreshRateHz[] = "refresh_rate_hz";
-int CuttlefishConfig::refresh_rate_hz() const {
-  return (*dictionary_)[kRefreshRateHz].asInt();
-}
-void CuttlefishConfig::set_refresh_rate_hz(int refresh_rate_hz) {
-  (*dictionary_)[kRefreshRateHz] = refresh_rate_hz;
-}
-
 void CuttlefishConfig::SetPath(const std::string& key,
                                const std::string& path) {
   if (!path.empty()) {
@@ -695,14 +690,6 @@
   return (*dictionary_)[kVhostNet].asBool();
 }
 
-static constexpr char kEthernet[] = "ethernet";
-void CuttlefishConfig::set_ethernet(bool ethernet) {
-  (*dictionary_)[kEthernet] = ethernet;
-}
-bool CuttlefishConfig::ethernet() const {
-  return (*dictionary_)[kEthernet].asBool();
-}
-
 static constexpr char kRecordScreen[] = "record_screen";
 void CuttlefishConfig::set_record_screen(bool record_screen) {
   (*dictionary_)[kRecordScreen] = record_screen;
diff --git a/host/libs/config/cuttlefish_config.h b/host/libs/config/cuttlefish_config.h
index 2b76c75..db3207e 100644
--- a/host/libs/config/cuttlefish_config.h
+++ b/host/libs/config/cuttlefish_config.h
@@ -52,6 +52,8 @@
 constexpr char kEthernetConnectedMessage[] =
     "VIRTUAL_DEVICE_NETWORK_ETHERNET_CONNECTED";
 constexpr char kScreenChangedMessage[] = "VIRTUAL_DEVICE_SCREEN_CHANGED";
+constexpr char kDisplayPowerModeChangedMessage[] =
+    "VIRTUAL_DEVICE_DISPLAY_POWER_MODE_CHANGED";
 constexpr char kInternalDirName[] = "internal";
 constexpr char kSharedDirName[] = "shared";
 constexpr char kCrosvmVarEmptyDir[] = "/var/empty";
@@ -101,15 +103,11 @@
   int memory_mb() const;
   void set_memory_mb(int memory_mb);
 
-  int dpi() const;
-  void set_dpi(int dpi);
-
-  int refresh_rate_hz() const;
-  void set_refresh_rate_hz(int refresh_rate_hz);
-
   struct DisplayConfig {
     int width;
     int height;
+    int dpi;
+    int refresh_rate_hz;
   };
 
   std::vector<DisplayConfig> display_configs() const;
@@ -294,9 +292,6 @@
   void set_vhost_net(bool vhost_net);
   bool vhost_net() const;
 
-  void set_ethernet(bool ethernet);
-  bool ethernet() const;
-
   void set_record_screen(bool record_screen);
   bool record_screen() const;
 
@@ -370,6 +365,8 @@
     int rootcanal_hci_port() const;
     int rootcanal_link_port() const;
     int rootcanal_test_port() const;
+    // Port number to connect to the camera hal on the guest
+    int camera_server_port() const;
     std::string rootcanal_config_file() const;
     std::string rootcanal_default_commands_file() const;
 
@@ -396,7 +393,7 @@
 
     std::string instance_internal_dir() const;
 
-    std::string touch_socket_path() const;
+    std::string touch_socket_path(int screen_idx) const;
     std::string keyboard_socket_path() const;
     std::string switches_socket_path() const;
     std::string frames_socket_path() const;
@@ -485,6 +482,7 @@
     void set_rootcanal_hci_port(int rootcanal_hci_port);
     void set_rootcanal_link_port(int rootcanal_link_port);
     void set_rootcanal_test_port(int rootcanal_test_port);
+    void set_camera_server_port(int camera_server_port);
     void set_rootcanal_config_file(const std::string& rootcanal_config_file);
     void set_rootcanal_default_commands_file(
         const std::string& rootcanal_default_commands_file);
diff --git a/host/libs/config/cuttlefish_config_instance.cpp b/host/libs/config/cuttlefish_config_instance.cpp
index 97b3503..14e7037 100644
--- a/host/libs/config/cuttlefish_config_instance.cpp
+++ b/host/libs/config/cuttlefish_config_instance.cpp
@@ -389,6 +389,15 @@
   (*Dictionary())[kRootcanalTestPort] = rootcanal_test_port;
 }
 
+static constexpr char kCameraServerPort[] = "camera_server_port";
+int CuttlefishConfig::InstanceSpecific::camera_server_port() const {
+  return (*Dictionary())[kCameraServerPort].asInt();
+}
+void CuttlefishConfig::MutableInstanceSpecific::set_camera_server_port(
+    int camera_server_port) {
+  (*Dictionary())[kCameraServerPort] = camera_server_port;
+}
+
 static constexpr char kRootcanalConfigFile[] = "rootcanal_config_file";
 std::string CuttlefishConfig::InstanceSpecific::rootcanal_config_file() const {
   return (*Dictionary())[kRootcanalConfigFile].asString();
@@ -429,8 +438,10 @@
   return (*Dictionary())[kStartSigServer].asBool();
 }
 
-std::string CuttlefishConfig::InstanceSpecific::touch_socket_path() const {
-  return PerInstanceInternalPath("touch.sock");
+std::string CuttlefishConfig::InstanceSpecific::touch_socket_path(
+    int screen_idx) const {
+  return PerInstanceInternalPath(
+      ("touch_" + std::to_string(screen_idx) + ".sock").c_str());
 }
 
 std::string CuttlefishConfig::InstanceSpecific::keyboard_socket_path() const {
diff --git a/host/libs/config/kernel_args.cpp b/host/libs/config/kernel_args.cpp
index 421950a..f5393d5 100644
--- a/host/libs/config/kernel_args.cpp
+++ b/host/libs/config/kernel_args.cpp
@@ -80,7 +80,7 @@
         vm_manager_cmdline.push_back("ramoops.dump_oops=1");
       } else {
         // crosvm requires these additional parameters on x86_64 in bootloader mode
-        AppendVector(&vm_manager_cmdline, {"pci=noacpi", "reboot=k"});
+        AppendVector(&vm_manager_cmdline, {"reboot=k"});
       }
     }
   }
diff --git a/host/libs/confui/session.cc b/host/libs/confui/session.cc
index 3d365c6..891250d 100644
--- a/host/libs/confui/session.cc
+++ b/host/libs/confui/session.cc
@@ -50,8 +50,13 @@
 
   ConfUiLog(DEBUG) << "actually trying to render the frame"
                    << thread::GetName();
-  auto raw_frame = reinterpret_cast<std::uint8_t*>(teeui_frame.data());
-  return screen_connector_.RenderConfirmationUi(display_num_, raw_frame);
+  auto frame_width = ScreenConnectorInfo::ScreenWidth(display_num_);
+  auto frame_height = ScreenConnectorInfo::ScreenHeight(display_num_);
+  auto frame_stride_bytes =
+      ScreenConnectorInfo::ScreenStrideBytes(display_num_);
+  auto frame_bytes = reinterpret_cast<std::uint8_t*>(teeui_frame.data());
+  return screen_connector_.RenderConfirmationUi(
+      display_num_, frame_width, frame_height, frame_stride_bytes, frame_bytes);
 }
 
 bool Session::IsSuspended() const {
diff --git a/host/libs/screen_connector/screen_connector.h b/host/libs/screen_connector/screen_connector.h
index de55b0e..ca6b155 100644
--- a/host/libs/screen_connector/screen_connector.h
+++ b/host/libs/screen_connector/screen_connector.h
@@ -64,7 +64,9 @@
    * call.
    */
   using GenerateProcessedFrameCallback = std::function<void(
-      std::uint32_t /*display_number*/, std::uint8_t* /*frame_pixels*/,
+      std::uint32_t /*display_number*/, std::uint32_t /*frame_width*/,
+      std::uint32_t /*frame_height*/, std::uint32_t /*frame_stride_bytes*/,
+      std::uint8_t* /*frame_bytes*/,
       /* ScImpl enqueues this type into the Q */
       ProcessedFrameType& msg)>;
 
@@ -94,14 +96,27 @@
     std::lock_guard<std::mutex> lock(streamer_callback_mutex_);
     callback_from_streamer_ = std::move(frame_callback);
     streamer_callback_set_cv_.notify_all();
-    /*
-     * the first WaitForAtLeastOneClientConnection() call from VNC requires the
-     * Android-frame-processing thread starts beforehands (b/178504150)
-     */
-    if (!sc_android_frame_fetching_thread_.joinable()) {
-      sc_android_frame_fetching_thread_ = cuttlefish::confui::thread::RunThread(
-          "AndroidFetcher", &ScreenConnector::AndroidFrameFetchingLoop, this);
-    }
+
+    sc_android_src_->SetFrameCallback(
+        [this](std::uint32_t display_number, std::uint32_t frame_w,
+               std::uint32_t frame_h, std::uint32_t frame_stride_bytes,
+               std::uint8_t* frame_bytes) {
+          const bool is_confui_mode = host_mode_ctrl_.IsConfirmatioUiMode();
+          if (is_confui_mode) {
+            return;
+          }
+
+          ProcessedFrameType processed_frame;
+
+          {
+            std::lock_guard<std::mutex> lock(streamer_callback_mutex_);
+            callback_from_streamer_(display_number, frame_w, frame_h,
+                                    frame_stride_bytes, frame_bytes,
+                                    processed_frame);
+          }
+
+          sc_android_queue_.PushBack(std::move(processed_frame));
+        });
   }
 
   bool IsCallbackSet() const override {
@@ -155,43 +170,6 @@
     }
   }
 
-  [[noreturn]] void AndroidFrameFetchingLoop() {
-    unsigned long long int loop_cnt = 0;
-    cuttlefish::confui::thread::Set("AndroidFrameFetcher",
-                                    std::this_thread::get_id());
-    while (true) {
-      loop_cnt++;
-      ProcessedFrameType processed_frame;
-      decltype(callback_from_streamer_) cp_of_streamer_callback;
-      {
-        std::lock_guard<std::mutex> lock(streamer_callback_mutex_);
-        cp_of_streamer_callback = callback_from_streamer_;
-      }
-      GenerateProcessedFrameCallbackImpl callback_for_sc_impl =
-          std::bind(cp_of_streamer_callback, std::placeholders::_1,
-                    std::placeholders::_2, std::ref(processed_frame));
-      ConfUiLog(VERBOSE) << cuttlefish::confui::thread::GetName(
-                                std::this_thread::get_id())
-                         << " calling Android OnNextFrame. "
-                         << " at loop #" << loop_cnt;
-      bool flag = sc_android_src_->OnNextFrame(callback_for_sc_impl);
-      processed_frame.is_success_ = flag && processed_frame.is_success_;
-      const bool is_confui_mode = host_mode_ctrl_.IsConfirmatioUiMode();
-      if (!is_confui_mode) {
-        ConfUiLog(VERBOSE) << cuttlefish::confui::thread::GetName(
-                                  std::this_thread::get_id())
-                           << "is sending an Android Frame at loop_cnt #"
-                           << loop_cnt;
-        sc_android_queue_.PushBack(std::move(processed_frame));
-        continue;
-      }
-      ConfUiLog(VERBOSE) << cuttlefish::confui::thread::GetName(
-                                std::this_thread::get_id())
-                         << "is skipping an Android Frame at loop_cnt #"
-                         << loop_cnt;
-    }
-  }
-
   /**
    * ConfUi calls this when it has frames to render
    *
@@ -199,8 +177,11 @@
    * Android guest frames if Confirmation UI HAL is not active.
    *
    */
-  bool RenderConfirmationUi(const std::uint32_t display,
-                            std::uint8_t* raw_frame) override {
+  bool RenderConfirmationUi(std::uint32_t display_number,
+                            std::uint32_t frame_width,
+                            std::uint32_t frame_height,
+                            std::uint32_t frame_stride_bytes,
+                            std::uint8_t* frame_bytes) override {
     render_confui_cnt_++;
     // wait callback is not set, the streamer is not ready
     // return with LOG(ERROR)
@@ -213,7 +194,8 @@
     ConfUiLog(DEBUG) << this_thread_name
                      << "is sending a #" + std::to_string(render_confui_cnt_)
                      << "Conf UI frame";
-    callback_from_streamer_(display, raw_frame, processed_frame);
+    callback_from_streamer_(display_number, frame_width, frame_height,
+                            frame_stride_bytes, frame_bytes, processed_frame);
     // now add processed_frame to the queue
     sc_confui_queue_.PushBack(std::move(processed_frame));
     return true;
@@ -249,7 +231,6 @@
   ScreenConnectorQueue<ProcessedFrameType> sc_android_queue_;
   ScreenConnectorQueue<ProcessedFrameType> sc_confui_queue_;
   GenerateProcessedFrameCallback callback_from_streamer_;
-  std::thread sc_android_frame_fetching_thread_;
   std::mutex streamer_callback_mutex_; // mutex to set & read callback_from_streamer_
   std::condition_variable streamer_callback_set_cv_;
 };
diff --git a/host/libs/screen_connector/screen_connector_common.h b/host/libs/screen_connector/screen_connector_common.h
index 48c7c76..1df6d86 100644
--- a/host/libs/screen_connector/screen_connector_common.h
+++ b/host/libs/screen_connector/screen_connector_common.h
@@ -34,16 +34,20 @@
 };
 
 // this callback type is going directly to socket-based or wayland ScreenConnector
-using GenerateProcessedFrameCallbackImpl = std::function<void(std::uint32_t /*display_number*/,
-                                                              std::uint8_t* /*frame_pixels*/)>;
+using GenerateProcessedFrameCallbackImpl =
+    std::function<void(std::uint32_t /*display_number*/,      //
+                       std::uint32_t /*frame_width*/,         //
+                       std::uint32_t /*frame_height*/,        //
+                       std::uint32_t /*frame_stride_bytes*/,  //
+                       std::uint8_t* /*frame_pixels*/)>;
 
 class ScreenConnectorSource {
  public:
   virtual ~ScreenConnectorSource() = default;
   // Runs the given callback on the next available frame after the given
   // frame number and returns true if successful.
-  virtual bool OnNextFrame(
-      const GenerateProcessedFrameCallbackImpl& frame_callback) = 0;
+  virtual void SetFrameCallback(
+      GenerateProcessedFrameCallbackImpl frame_callback) = 0;
   virtual void ReportClientsConnected(bool /*have_clients*/) { /* ignore by default */ }
   ScreenConnectorSource() = default;
 };
@@ -83,8 +87,11 @@
 };
 
 struct ScreenConnectorFrameRenderer {
-  virtual bool RenderConfirmationUi(const std::uint32_t display,
-                                    std::uint8_t* raw_frame) = 0;
+  virtual bool RenderConfirmationUi(std::uint32_t display_number,
+                                    std::uint32_t frame_width,
+                                    std::uint32_t frame_height,
+                                    std::uint32_t frame_stride_bytes,
+                                    std::uint8_t* frame_bytes) = 0;
   virtual bool IsCallbackSet() const = 0;
   virtual ~ScreenConnectorFrameRenderer() = default;
 };
diff --git a/host/libs/screen_connector/wayland_screen_connector.cpp b/host/libs/screen_connector/wayland_screen_connector.cpp
index 7eb6642..dbd4052 100644
--- a/host/libs/screen_connector/wayland_screen_connector.cpp
+++ b/host/libs/screen_connector/wayland_screen_connector.cpp
@@ -33,10 +33,9 @@
   server_.reset(new wayland::WaylandServer(wayland_fd));
 }
 
-bool WaylandScreenConnector::OnNextFrame(
-    const GenerateProcessedFrameCallbackImpl& frame_callback) {
-  server_->OnNextFrame(frame_callback);
-  return true;
+void WaylandScreenConnector::SetFrameCallback(
+    GenerateProcessedFrameCallbackImpl frame_callback) {
+  server_->SetFrameCallback(std::move(frame_callback));
 }
 
 }  // namespace cuttlefish
diff --git a/host/libs/screen_connector/wayland_screen_connector.h b/host/libs/screen_connector/wayland_screen_connector.h
index 00e7ca0..36ed322 100644
--- a/host/libs/screen_connector/wayland_screen_connector.h
+++ b/host/libs/screen_connector/wayland_screen_connector.h
@@ -28,8 +28,8 @@
  public:
   WaylandScreenConnector(int frames_fd);
 
-  bool OnNextFrame(
-      const GenerateProcessedFrameCallbackImpl& frame_callback) override;
+  void SetFrameCallback(
+      GenerateProcessedFrameCallbackImpl frame_callback) override;
 
  private:
   std::unique_ptr<wayland::WaylandServer> server_;
diff --git a/host/libs/vm_manager/crosvm_manager.cpp b/host/libs/vm_manager/crosvm_manager.cpp
index 82aba2d..e424fd5 100644
--- a/host/libs/vm_manager/crosvm_manager.cpp
+++ b/host/libs/vm_manager/crosvm_manager.cpp
@@ -220,23 +220,20 @@
     crosvm_cmd.AddParameter("--gdb=", config.gdb_port());
   }
 
-  auto display_configs = config.display_configs();
-  CHECK_GE(display_configs.size(), 1);
-  auto display_config = display_configs[0];
-
   auto gpu_mode = config.gpu_mode();
-
   if (gpu_mode == kGpuModeGuestSwiftshader) {
-    crosvm_cmd.AddParameter("--gpu=2D,",
-                            "width=", display_config.width, ",",
-                            "height=", display_config.height);
+    crosvm_cmd.AddParameter("--gpu=2D");
   } else if (gpu_mode == kGpuModeDrmVirgl || gpu_mode == kGpuModeGfxStream) {
     crosvm_cmd.AddParameter(gpu_mode == kGpuModeGfxStream ?
                                 "--gpu=gfxstream," : "--gpu=",
-                            "width=", display_config.width, ",",
-                            "height=", display_config.height, ",",
                             "egl=true,surfaceless=true,glx=false,gles=true");
   }
+
+  for (const auto& display_config : config.display_configs()) {
+    crosvm_cmd.AddParameter("--gpu-display=", "width=", display_config.width,
+                            ",", "height=", display_config.height);
+  }
+
   crosvm_cmd.AddParameter("--wayland-sock=", instance.frames_socket_path());
 
   // crosvm_cmd.AddParameter("--null-audio");
@@ -256,17 +253,26 @@
   if (config.enable_vnc_server() || config.enable_webrtc()) {
     auto touch_type_parameter =
         config.enable_webrtc() ? "--multi-touch=" : "--single-touch=";
-    crosvm_cmd.AddParameter(touch_type_parameter, instance.touch_socket_path(),
-                            ":", display_config.width, ":",
-                            display_config.height);
+
+    auto display_configs = config.display_configs();
+    CHECK_GE(display_configs.size(), 1);
+
+    for (int i = 0; i < display_configs.size(); ++i) {
+      auto display_config = display_configs[i];
+
+      crosvm_cmd.AddParameter(touch_type_parameter,
+                              instance.touch_socket_path(i), ":",
+                              display_config.width, ":", display_config.height);
+    }
     crosvm_cmd.AddParameter("--keyboard=", instance.keyboard_socket_path());
   }
   if (config.enable_webrtc()) {
     crosvm_cmd.AddParameter("--switches=", instance.switches_socket_path());
   }
 
-  auto wifi_tap = AddTapFdParameter(&crosvm_cmd, instance.wifi_tap_name());
   AddTapFdParameter(&crosvm_cmd, instance.mobile_tap_name());
+  AddTapFdParameter(&crosvm_cmd, instance.ethernet_tap_name());
+  auto wifi_tap = AddTapFdParameter(&crosvm_cmd, instance.wifi_tap_name());
 
   if (FileExists(instance.access_kregistry_path())) {
     crosvm_cmd.AddParameter("--rw-pmem-device=",
@@ -376,16 +382,10 @@
       << VmManager::kMaxDisks + VmManager::kDefaultNumHvcs << " devices";
 
   if (config.enable_audio()) {
-    crosvm_cmd.AddParameter("--ac97=backend=vios,server=" +
+    crosvm_cmd.AddParameter("--sound=",
                             config.ForDefaultInstance().audio_server_path());
   }
 
-  // TODO(b/172286896): This is temporarily optional, but should be made
-  // unconditional and moved up to the other network devices area
-  if (config.ethernet()) {
-    AddTapFdParameter(&crosvm_cmd, instance.ethernet_tap_name());
-  }
-
   // TODO(b/162071003): virtiofs crashes without sandboxing, this should be fixed
   if (config.enable_sandbox()) {
     // Set up directory shared with virtiofs
diff --git a/host/libs/vm_manager/qemu_manager.cpp b/host/libs/vm_manager/qemu_manager.cpp
index f26818a..c8c1084 100644
--- a/host/libs/vm_manager/qemu_manager.cpp
+++ b/host/libs/vm_manager/qemu_manager.cpp
@@ -432,19 +432,26 @@
   qemu_cmd.AddParameter("virtio-balloon-pci-non-transitional,id=balloon0");
 
   qemu_cmd.AddParameter("-netdev");
-  qemu_cmd.AddParameter("tap,id=hostnet0,ifname=", instance.wifi_tap_name(),
+  qemu_cmd.AddParameter("tap,id=hostnet0,ifname=", instance.mobile_tap_name(),
                         ",script=no,downscript=no", vhost_net);
 
   qemu_cmd.AddParameter("-device");
   qemu_cmd.AddParameter("virtio-net-pci-non-transitional,netdev=hostnet0,id=net0");
 
   qemu_cmd.AddParameter("-netdev");
-  qemu_cmd.AddParameter("tap,id=hostnet1,ifname=", instance.mobile_tap_name(),
+  qemu_cmd.AddParameter("tap,id=hostnet1,ifname=", instance.ethernet_tap_name(),
                         ",script=no,downscript=no", vhost_net);
 
   qemu_cmd.AddParameter("-device");
   qemu_cmd.AddParameter("virtio-net-pci-non-transitional,netdev=hostnet1,id=net1");
 
+  qemu_cmd.AddParameter("-netdev");
+  qemu_cmd.AddParameter("tap,id=hostnet2,ifname=", instance.wifi_tap_name(),
+                        ",script=no,downscript=no", vhost_net);
+
+  qemu_cmd.AddParameter("-device");
+  qemu_cmd.AddParameter("virtio-net-pci-non-transitional,netdev=hostnet2,id=net2");
+
   qemu_cmd.AddParameter("-device");
   qemu_cmd.AddParameter("virtio-gpu-pci,id=gpu0");
 
@@ -461,17 +468,6 @@
   qemu_cmd.AddParameter("-device");
   qemu_cmd.AddParameter("AC97");
 
-  // TODO(b/172286896): This is temporarily optional, but should be made
-  // unconditional and moved up to the other network devices area
-  if (config.ethernet()) {
-    qemu_cmd.AddParameter("-netdev");
-    qemu_cmd.AddParameter("tap,id=hostnet2,ifname=", instance.ethernet_tap_name(),
-                          ",script=no,downscript=no", vhost_net);
-
-    qemu_cmd.AddParameter("-device");
-    qemu_cmd.AddParameter("virtio-net-pci-non-transitional,netdev=hostnet2,id=net2");
-  }
-
   qemu_cmd.AddParameter("-device");
   qemu_cmd.AddParameter("qemu-xhci,id=xhci");
 
diff --git a/host/libs/wayland/Android.bp b/host/libs/wayland/Android.bp
index 4be6528..12700f3 100644
--- a/host/libs/wayland/Android.bp
+++ b/host/libs/wayland/Android.bp
@@ -28,6 +28,7 @@
         "wayland_subcompositor.cpp",
         "wayland_surface.cpp",
         "wayland_surfaces.cpp",
+        "wayland_virtio_gpu_metadata.cpp",
     ],
     shared_libs: [
         "libbase",
@@ -37,6 +38,7 @@
     static_libs: [
         "libdrm",
         "libffi",
+        "libwayland_crosvm_gpu_display_extension_server_protocols",
         "libwayland_server",
         "libwayland_extension_server_protocols",
     ],
diff --git a/host/libs/wayland/wayland_compositor.cpp b/host/libs/wayland/wayland_compositor.cpp
index 6179092..4e2f8f5 100644
--- a/host/libs/wayland/wayland_compositor.cpp
+++ b/host/libs/wayland/wayland_compositor.cpp
@@ -182,7 +182,10 @@
   .damage_buffer = surface_damage_buffer,
 };
 
-void surface_destroy_resource_callback(struct wl_resource*) {}
+void surface_destroy_resource_callback(struct wl_resource* surface_resource) {
+  Surface* surface = GetUserData<Surface>(surface_resource);
+  delete surface;
+}
 
 void compositor_create_surface(wl_client* client,
                                wl_resource* compositor,
@@ -191,12 +194,8 @@
                << " compositor=" << compositor
                << " id=" << id;
 
-  // Wayland seems to use a single global id space for all objects.
-  static std::atomic<std::uint32_t> sNextDisplayId{0};
-  uint32_t display_id = sNextDisplayId++;
-
   Surfaces* surfaces = GetUserData<Surfaces>(compositor);
-  Surface* surface = surfaces->GetOrCreateSurface(display_id);
+  Surface* surface = new Surface(*surfaces);
 
   wl_resource* surface_resource = wl_resource_create(
       client, &wl_surface_interface, wl_resource_get_version(compositor), id);
diff --git a/host/libs/wayland/wayland_server.cpp b/host/libs/wayland/wayland_server.cpp
index 4dc4b88..7dbc7f7 100644
--- a/host/libs/wayland/wayland_server.cpp
+++ b/host/libs/wayland/wayland_server.cpp
@@ -28,6 +28,7 @@
 #include "host/libs/wayland/wayland_subcompositor.h"
 #include "host/libs/wayland/wayland_surface.h"
 #include "host/libs/wayland/wayland_utils.h"
+#include "host/libs/wayland/wayland_virtio_gpu_metadata.h"
 
 namespace wayland {
 namespace internal {
@@ -79,6 +80,8 @@
   wl_display_init_shm(server_state_->display_);
 
   BindCompositorInterface(server_state_->display_, &server_state_->surfaces_);
+  BindVirtioGpuMetadataInterface(server_state_->display_,
+                                 &server_state_->surfaces_);
   BindDmabufInterface(server_state_->display_);
   BindSubcompositorInterface(server_state_->display_);
   BindSeatInterface(server_state_->display_);
@@ -94,8 +97,8 @@
   wl_display_destroy(server_state_->display_);
 }
 
-void WaylandServer::OnNextFrame(const Surfaces::FrameCallback& callback) {
-  server_state_->surfaces_.OnNextFrame(callback);
+void WaylandServer::SetFrameCallback(Surfaces::FrameCallback callback) {
+  server_state_->surfaces_.SetFrameCallback(std::move(callback));
 }
 
 }  // namespace wayland
diff --git a/host/libs/wayland/wayland_server.h b/host/libs/wayland/wayland_server.h
index 3149481..24ee68c 100644
--- a/host/libs/wayland/wayland_server.h
+++ b/host/libs/wayland/wayland_server.h
@@ -48,8 +48,9 @@
     WaylandServer(WaylandServer&& rhs) = delete;
     WaylandServer& operator=(WaylandServer&& rhs) = delete;
 
-    // Blocks until the given callback is run on the next frame available.
-    void OnNextFrame(const Surfaces::FrameCallback& callback);
+    // Registers the callback that will be run whenever a new frame is
+    // available.
+    void SetFrameCallback(Surfaces::FrameCallback callback);
 
    private:
     void ServerLoop(int wayland_socket_fd);
diff --git a/host/libs/wayland/wayland_shell.cpp b/host/libs/wayland/wayland_shell.cpp
index 439fc62..424c878 100644
--- a/host/libs/wayland/wayland_shell.cpp
+++ b/host/libs/wayland/wayland_shell.cpp
@@ -20,35 +20,29 @@
 
 #include <wayland-server-core.h>
 #include <wayland-server-protocol.h>
-#include <xdg-shell-unstable-v6-server-protocol.h>
+#include <xdg-shell-server-protocol.h>
 
 namespace wayland {
 namespace {
 
-
-void zxdg_positioner_v6_destroy(wl_client*, wl_resource* positioner) {
+void xdg_positioner_destroy(wl_client*, wl_resource* positioner) {
   LOG(VERBOSE) << __FUNCTION__
                << " positioner=" << positioner;
 
   wl_resource_destroy(positioner);
 }
 
-void zxdg_positioner_v6_set_size(wl_client*,
-                                 wl_resource* positioner,
-                                 int32_t w,
-                                 int32_t h) {
+void xdg_positioner_set_size(wl_client*, wl_resource* positioner, int32_t w,
+                             int32_t h) {
   LOG(VERBOSE) << __FUNCTION__
                << " positioner=" << positioner
                << " w=" << w
                << " h=" << h;
 }
 
-void zxdg_positioner_v6_set_anchor_rect(wl_client*,
-                                        wl_resource* positioner,
-                                        int32_t x,
-                                        int32_t y,
-                                        int32_t w,
-                                        int32_t h) {
+void xdg_positioner_set_anchor_rect(wl_client*, wl_resource* positioner,
+                                    int32_t x, int32_t y, int32_t w,
+                                    int32_t h) {
   LOG(VERBOSE) << __FUNCTION__
                << " positioner=" << positioner
                << " x=" << x
@@ -57,87 +51,76 @@
                << " h=" << h;
 }
 
-void zxdg_positioner_v6_set_anchor(wl_client*,
-                                   wl_resource* positioner,
-                                   uint32_t anchor) {
+void xdg_positioner_set_anchor(wl_client*, wl_resource* positioner,
+                               uint32_t anchor) {
   LOG(VERBOSE) << __FUNCTION__
                << " positioner=" << positioner
                << " anchor=" << anchor;
 }
 
-void zxdg_positioner_v6_set_gravity(wl_client*,
-                                    wl_resource* positioner,
-                                    uint32_t gravity) {
+void xdg_positioner_set_gravity(wl_client*, wl_resource* positioner,
+                                uint32_t gravity) {
   LOG(VERBOSE) << __FUNCTION__
                << " positioner=" << positioner
                << " gravity=" << gravity;
 }
 
-void zxdg_positioner_v6_set_constraint_adjustment(wl_client*,
-                                                  wl_resource* positioner,
-                                                  uint32_t adjustment) {
+void xdg_positioner_set_constraint_adjustment(wl_client*,
+                                              wl_resource* positioner,
+                                              uint32_t adjustment) {
   LOG(VERBOSE) << __FUNCTION__
                << " positioner=" << positioner
                << " adjustment=" << adjustment;
 }
 
-void zxdg_positioner_v6_set_offset(wl_client*,
-                                   wl_resource* positioner,
-                                   int32_t x,
-                                   int32_t y) {
+void xdg_positioner_set_offset(wl_client*, wl_resource* positioner, int32_t x,
+                               int32_t y) {
   LOG(VERBOSE) << __FUNCTION__
                << " positioner=" << positioner
                << " x=" << x
                << " y=" << y;
 }
 
-const struct zxdg_positioner_v6_interface
-    zxdg_positioner_v6_implementation = {
-        .destroy = zxdg_positioner_v6_destroy,
-        .set_size = zxdg_positioner_v6_set_size,
-        .set_anchor_rect = zxdg_positioner_v6_set_anchor_rect,
-        .set_anchor = zxdg_positioner_v6_set_anchor,
-        .set_gravity = zxdg_positioner_v6_set_gravity,
-        .set_constraint_adjustment = zxdg_positioner_v6_set_constraint_adjustment,
-        .set_offset = zxdg_positioner_v6_set_offset};
+const struct xdg_positioner_interface xdg_positioner_implementation = {
+    .destroy = xdg_positioner_destroy,
+    .set_size = xdg_positioner_set_size,
+    .set_anchor_rect = xdg_positioner_set_anchor_rect,
+    .set_anchor = xdg_positioner_set_anchor,
+    .set_gravity = xdg_positioner_set_gravity,
+    .set_constraint_adjustment = xdg_positioner_set_constraint_adjustment,
+    .set_offset = xdg_positioner_set_offset};
 
-void zxdg_toplevel_v6_destroy(wl_client*, wl_resource* toplevel) {
+void xdg_toplevel_destroy(wl_client*, wl_resource* toplevel) {
   LOG(VERBOSE) << __FUNCTION__
                << " toplevel=" << toplevel;
 
   wl_resource_destroy(toplevel);
 }
 
-void zxdg_toplevel_v6_set_parent(wl_client*,
-                                 wl_resource* toplevel,
-                                 wl_resource* parent_toplevel) {
+void xdg_toplevel_set_parent(wl_client*, wl_resource* toplevel,
+                             wl_resource* parent_toplevel) {
   LOG(VERBOSE) << __FUNCTION__
                << " toplevel=" << toplevel
                << " parent_toplevel=" << parent_toplevel;
 }
 
-void zxdg_toplevel_v6_set_title(wl_client*,
-                                wl_resource* toplevel,
-                                const char* title) {
+void xdg_toplevel_set_title(wl_client*, wl_resource* toplevel,
+                            const char* title) {
   LOG(VERBOSE) << __FUNCTION__
                << " toplevel=" << toplevel
                << " title=" << title;
 }
 
-void zxdg_toplevel_v6_set_app_id(wl_client*,
-                                 wl_resource* toplevel,
-                                 const char* app) {
+void xdg_toplevel_set_app_id(wl_client*, wl_resource* toplevel,
+                             const char* app) {
   LOG(VERBOSE) << __FUNCTION__
                << " toplevel=" << toplevel
                << " app=" << app;
 }
 
-void zxdg_toplevel_v6_show_window_menu(wl_client*,
-                                       wl_resource* toplevel,
-                                       wl_resource* seat,
-                                       uint32_t serial,
-                                       int32_t x,
-                                       int32_t y) {
+void xdg_toplevel_show_window_menu(wl_client*, wl_resource* toplevel,
+                                   wl_resource* seat, uint32_t serial,
+                                   int32_t x, int32_t y) {
   LOG(VERBOSE) << __FUNCTION__
                << " toplevel=" << toplevel
                << " seat=" << seat
@@ -146,21 +129,16 @@
                << " y=" << y;
 }
 
-void zxdg_toplevel_v6_move(wl_client*,
-                           wl_resource* toplevel,
-                           wl_resource* seat,
-                           uint32_t serial) {
+void xdg_toplevel_move(wl_client*, wl_resource* toplevel, wl_resource* seat,
+                       uint32_t serial) {
   LOG(VERBOSE) << __FUNCTION__
                << " toplevel=" << toplevel
                << " seat=" << seat
                << " serial=" << serial;
 }
 
-void zxdg_toplevel_v6_resize(wl_client*,
-                             wl_resource* toplevel,
-                             wl_resource* seat,
-                             uint32_t serial,
-                             uint32_t edges) {
+void xdg_toplevel_resize(wl_client*, wl_resource* toplevel, wl_resource* seat,
+                         uint32_t serial, uint32_t edges) {
   LOG(VERBOSE) << __FUNCTION__
                << " toplevel=" << toplevel
                << " seat=" << seat
@@ -168,93 +146,83 @@
                << " edges=" << edges;
 }
 
-void zxdg_toplevel_v6_set_max_size(wl_client*,
-                                   wl_resource* toplevel,
-                                   int32_t w,
-                                   int32_t h) {
+void xdg_toplevel_set_max_size(wl_client*, wl_resource* toplevel, int32_t w,
+                               int32_t h) {
   LOG(VERBOSE) << __FUNCTION__
                << " toplevel=" << toplevel
                << " w=" << w
                << " h=" << h;
 }
 
-void zxdg_toplevel_v6_set_min_size(wl_client*,
-                                   wl_resource* toplevel,
-                                   int32_t w,
-                                   int32_t h) {
+void xdg_toplevel_set_min_size(wl_client*, wl_resource* toplevel, int32_t w,
+                               int32_t h) {
   LOG(VERBOSE) << __FUNCTION__
                << " toplevel=" << toplevel
                << " w=" << w
                << " h=" << h;
 }
 
-void zxdg_toplevel_v6_set_maximized(wl_client*, wl_resource* toplevel) {
+void xdg_toplevel_set_maximized(wl_client*, wl_resource* toplevel) {
   LOG(VERBOSE) << __FUNCTION__
                << " toplevel=" << toplevel;
 }
 
-void zxdg_toplevel_v6_unset_maximized(wl_client*, wl_resource* toplevel) {
+void xdg_toplevel_unset_maximized(wl_client*, wl_resource* toplevel) {
   LOG(VERBOSE) << __FUNCTION__
                << " toplevel=" << toplevel;
 }
 
-void zxdg_toplevel_v6_set_fullscreen(wl_client*,
-                                     wl_resource* toplevel,
-                                     wl_resource*) {
+void xdg_toplevel_set_fullscreen(wl_client*, wl_resource* toplevel,
+                                 wl_resource*) {
   LOG(VERBOSE) << __FUNCTION__
                << " toplevel=" << toplevel;
 }
 
-void zxdg_toplevel_v6_unset_fullscreen(wl_client*, wl_resource* toplevel) {
+void xdg_toplevel_unset_fullscreen(wl_client*, wl_resource* toplevel) {
   LOG(VERBOSE) << __FUNCTION__
                << " toplevel=" << toplevel;
 }
 
-void zxdg_toplevel_v6_set_minimized(wl_client*, wl_resource* toplevel) {
+void xdg_toplevel_set_minimized(wl_client*, wl_resource* toplevel) {
   LOG(VERBOSE) << __FUNCTION__
                << " toplevel=" << toplevel;
 }
 
-const struct zxdg_toplevel_v6_interface zxdg_toplevel_v6_implementation = {
-  .destroy = zxdg_toplevel_v6_destroy,
-  .set_parent = zxdg_toplevel_v6_set_parent,
-  .set_title = zxdg_toplevel_v6_set_title,
-  .set_app_id = zxdg_toplevel_v6_set_app_id,
-  .show_window_menu = zxdg_toplevel_v6_show_window_menu,
-  .move = zxdg_toplevel_v6_move,
-  .resize = zxdg_toplevel_v6_resize,
-  .set_max_size = zxdg_toplevel_v6_set_max_size,
-  .set_min_size = zxdg_toplevel_v6_set_min_size,
-  .set_maximized = zxdg_toplevel_v6_set_maximized,
-  .unset_maximized = zxdg_toplevel_v6_unset_maximized,
-  .set_fullscreen = zxdg_toplevel_v6_set_fullscreen,
-  .unset_fullscreen = zxdg_toplevel_v6_unset_fullscreen,
-  .set_minimized = zxdg_toplevel_v6_set_minimized
-};
+const struct xdg_toplevel_interface xdg_toplevel_implementation = {
+    .destroy = xdg_toplevel_destroy,
+    .set_parent = xdg_toplevel_set_parent,
+    .set_title = xdg_toplevel_set_title,
+    .set_app_id = xdg_toplevel_set_app_id,
+    .show_window_menu = xdg_toplevel_show_window_menu,
+    .move = xdg_toplevel_move,
+    .resize = xdg_toplevel_resize,
+    .set_max_size = xdg_toplevel_set_max_size,
+    .set_min_size = xdg_toplevel_set_min_size,
+    .set_maximized = xdg_toplevel_set_maximized,
+    .unset_maximized = xdg_toplevel_unset_maximized,
+    .set_fullscreen = xdg_toplevel_set_fullscreen,
+    .unset_fullscreen = xdg_toplevel_unset_fullscreen,
+    .set_minimized = xdg_toplevel_set_minimized};
 
-void zxdg_popup_v6_destroy(wl_client*, wl_resource* popup) {
+void xdg_popup_destroy(wl_client*, wl_resource* popup) {
   LOG(VERBOSE) << __FUNCTION__
                << " popup=" << popup;
 
   wl_resource_destroy(popup);
 }
 
-void zxdg_popup_v6_grab(wl_client*,
-                        wl_resource* popup,
-                        wl_resource* seat,
-                        uint32_t serial) {
+void xdg_popup_grab(wl_client*, wl_resource* popup, wl_resource* seat,
+                    uint32_t serial) {
   LOG(VERBOSE) << __FUNCTION__
                << " popup=" << popup
                << " seat=" << seat
                << " serial=" << serial;
 }
 
-const struct zxdg_popup_v6_interface zxdg_popup_v6_implementation = {
-    .destroy = zxdg_popup_v6_destroy,
-    .grab = zxdg_popup_v6_grab
-};
+const struct xdg_popup_interface xdg_popup_implementation = {
+    .destroy = xdg_popup_destroy, .grab = xdg_popup_grab};
 
-void zxdg_surface_v6_destroy(wl_client*, wl_resource* surface) {
+void xdg_surface_destroy(wl_client*, wl_resource* surface) {
   LOG(VERBOSE) << __FUNCTION__
                << " surface=" << surface;
 
@@ -263,28 +231,25 @@
 
 void toplevel_destroy_resource_callback(struct wl_resource*) {}
 
-void zxdg_surface_v6_get_toplevel(wl_client* client,
-                                  wl_resource* surface,
-                                  uint32_t id) {
+void xdg_surface_get_toplevel(wl_client* client, wl_resource* surface,
+                              uint32_t id) {
   LOG(VERBOSE) << __FUNCTION__
                << " surface=" << surface
                << " id=" << id;
 
   wl_resource* xdg_toplevel_resource =
-      wl_resource_create(client, &zxdg_toplevel_v6_interface, 1, id);
+      wl_resource_create(client, &xdg_toplevel_interface, 1, id);
 
   wl_resource_set_implementation(xdg_toplevel_resource,
-                                 &zxdg_toplevel_v6_implementation, nullptr,
+                                 &xdg_toplevel_implementation, nullptr,
                                  toplevel_destroy_resource_callback);
 }
 
 void popup_destroy_resource_callback(struct wl_resource*) {}
 
-void zxdg_surface_v6_get_popup(wl_client* client,
-                              wl_resource* surface,
-                              uint32_t id,
-                              wl_resource* parent_surface,
-                              wl_resource* positioner) {
+void xdg_surface_get_popup(wl_client* client, wl_resource* surface, uint32_t id,
+                           wl_resource* parent_surface,
+                           wl_resource* positioner) {
   LOG(VERBOSE) << __FUNCTION__
                << " surface=" << surface
                << " id=" << id
@@ -292,19 +257,15 @@
                << " positioner=" << positioner;
 
   wl_resource* xdg_popup_resource =
-      wl_resource_create(client, &zxdg_popup_v6_interface, 1, id);
+      wl_resource_create(client, &xdg_popup_interface, 1, id);
 
-  wl_resource_set_implementation(xdg_popup_resource,
-                                 &zxdg_popup_v6_implementation, nullptr,
-                                 popup_destroy_resource_callback);
+  wl_resource_set_implementation(xdg_popup_resource, &xdg_popup_implementation,
+                                 nullptr, popup_destroy_resource_callback);
 }
 
-void zxdg_surface_v6_set_window_geometry(wl_client*,
-                                         wl_resource* surface,
-                                         int32_t x,
-                                         int32_t y,
-                                         int32_t w,
-                                         int32_t h) {
+void xdg_surface_set_window_geometry(wl_client*, wl_resource* surface,
+                                     int32_t x, int32_t y, int32_t w,
+                                     int32_t h) {
   LOG(VERBOSE) << __FUNCTION__
                << " surface=" << surface
                << " x=" << x
@@ -313,23 +274,21 @@
                << " h=" << h;
 }
 
-void zxdg_surface_v6_ack_configure(wl_client*,
-                                   wl_resource* surface,
-                                   uint32_t serial) {
+void xdg_surface_ack_configure(wl_client*, wl_resource* surface,
+                               uint32_t serial) {
   LOG(VERBOSE) << __FUNCTION__
                << " surface=" << surface
                << " serial=" << serial;
 }
 
-const struct zxdg_surface_v6_interface zxdg_surface_v6_implementation = {
-    .destroy = zxdg_surface_v6_destroy,
-    .get_toplevel = zxdg_surface_v6_get_toplevel,
-    .get_popup = zxdg_surface_v6_get_popup,
-    .set_window_geometry = zxdg_surface_v6_set_window_geometry,
-    .ack_configure = zxdg_surface_v6_ack_configure
-};
+const struct xdg_surface_interface xdg_surface_implementation = {
+    .destroy = xdg_surface_destroy,
+    .get_toplevel = xdg_surface_get_toplevel,
+    .get_popup = xdg_surface_get_popup,
+    .set_window_geometry = xdg_surface_set_window_geometry,
+    .ack_configure = xdg_surface_ack_configure};
 
-void zxdg_shell_v6_destroy(wl_client*, wl_resource* shell) {
+void xdg_shell_destroy(wl_client*, wl_resource* shell) {
   LOG(VERBOSE) << __FUNCTION__
                << " shell=" << shell;
 
@@ -338,65 +297,60 @@
 
 void positioner_destroy_resource_callback(struct wl_resource*) {}
 
-void zxdg_shell_v6_create_positioner(wl_client* client,
-                                     wl_resource* shell,
-                                     uint32_t id) {
+void xdg_shell_create_positioner(wl_client* client, wl_resource* shell,
+                                 uint32_t id) {
   LOG(VERBOSE) << __FUNCTION__
                << " shell=" << shell
                << " id=" << id;
 
   wl_resource* positioner_resource =
-      wl_resource_create(client, &zxdg_positioner_v6_interface, 1, id);
+      wl_resource_create(client, &xdg_positioner_interface, 1, id);
 
   wl_resource_set_implementation(positioner_resource,
-                                 &zxdg_positioner_v6_implementation, nullptr,
+                                 &xdg_positioner_implementation, nullptr,
                                  positioner_destroy_resource_callback);
 }
 
 void surface_destroy_resource_callback(struct wl_resource*) {}
 
-void zxdg_shell_v6_get_xdg_surface(wl_client* client,
-                                   wl_resource* shell,
-                                   uint32_t id,
-                                   wl_resource* surface) {
+void xdg_shell_get_xdg_surface(wl_client* client, wl_resource* shell,
+                               uint32_t id, wl_resource* surface) {
   LOG(VERBOSE) << __FUNCTION__
                << " shell=" << shell
                << " id=" << id
                << " surface=" << surface;
 
   wl_resource* surface_resource =
-      wl_resource_create(client, &zxdg_surface_v6_interface, 1, id);
+      wl_resource_create(client, &xdg_surface_interface, 1, id);
 
-  wl_resource_set_implementation(surface_resource,
-                                 &zxdg_surface_v6_implementation, nullptr,
-                                 surface_destroy_resource_callback);
+  wl_resource_set_implementation(surface_resource, &xdg_surface_implementation,
+                                 nullptr, surface_destroy_resource_callback);
 }
 
-void zxdg_shell_v6_pong(wl_client*, wl_resource* shell, uint32_t serial) {
+void xdg_shell_pong(wl_client*, wl_resource* shell, uint32_t serial) {
   LOG(VERBOSE) << __FUNCTION__
                << " shell=" << shell
                << " serial=" << serial;
 }
 
-const struct zxdg_shell_v6_interface zxdg_shell_v6_implementation = {
-    .destroy = zxdg_shell_v6_destroy,
-    .create_positioner = zxdg_shell_v6_create_positioner,
-    .get_xdg_surface = zxdg_shell_v6_get_xdg_surface,
-    .pong = zxdg_shell_v6_pong
-};
+const struct xdg_wm_base_interface xdg_shell_implementation = {
+    .destroy = xdg_shell_destroy,
+    .create_positioner = xdg_shell_create_positioner,
+    .get_xdg_surface = xdg_shell_get_xdg_surface,
+    .pong = xdg_shell_pong};
 
 void bind_shell(wl_client* client, void* data, uint32_t version, uint32_t id) {
   wl_resource* shell_resource =
-      wl_resource_create(client, &zxdg_shell_v6_interface, version, id);
+      wl_resource_create(client, &xdg_wm_base_interface, version, id);
 
-  wl_resource_set_implementation(shell_resource,
-                                 &zxdg_shell_v6_implementation, data, nullptr);
+  wl_resource_set_implementation(shell_resource, &xdg_shell_implementation,
+                                 data, nullptr);
 }
 
 }  // namespace
 
 void BindShellInterface(wl_display* display) {
-  wl_global_create(display, &zxdg_shell_v6_interface, 1, nullptr, bind_shell);
+  wl_global_create(display, &xdg_wm_base_interface, 1, nullptr, bind_shell);
 }
 
 }  // namespace wayland
\ No newline at end of file
diff --git a/host/libs/wayland/wayland_surface.cpp b/host/libs/wayland/wayland_surface.cpp
index e9abece..51a3b42 100644
--- a/host/libs/wayland/wayland_surface.cpp
+++ b/host/libs/wayland/wayland_surface.cpp
@@ -23,8 +23,7 @@
 
 namespace wayland {
 
-Surface::Surface(std::uint32_t display_number, Surfaces& surfaces)
-    : display_number_(display_number), surfaces_(surfaces) {}
+Surface::Surface(Surfaces& surfaces) : surfaces_(surfaces) {}
 
 void Surface::SetRegion(const Region& region) {
   std::unique_lock<std::mutex> lock(state_mutex_);
@@ -45,22 +44,28 @@
     return;
   }
 
-  struct wl_shm_buffer* shm_buffer = wl_shm_buffer_get(state_.current_buffer);
-  CHECK(shm_buffer != nullptr);
+  if (state_.virtio_gpu_metadata_.scanout_id.has_value()) {
+    const uint32_t display_number = *state_.virtio_gpu_metadata_.scanout_id;
 
-  wl_shm_buffer_begin_access(shm_buffer);
+    struct wl_shm_buffer* shm_buffer = wl_shm_buffer_get(state_.current_buffer);
+    CHECK(shm_buffer != nullptr);
 
-  const int32_t buffer_w = wl_shm_buffer_get_width(shm_buffer);
-  CHECK(buffer_w == state_.region.w);
-  const int32_t buffer_h = wl_shm_buffer_get_height(shm_buffer);
-  CHECK(buffer_h == state_.region.h);
+    wl_shm_buffer_begin_access(shm_buffer);
 
-  uint8_t* buffer_pixels =
-      reinterpret_cast<uint8_t*>(wl_shm_buffer_get_data(shm_buffer));
+    const int32_t buffer_w = wl_shm_buffer_get_width(shm_buffer);
+    CHECK(buffer_w == state_.region.w);
+    const int32_t buffer_h = wl_shm_buffer_get_height(shm_buffer);
+    CHECK(buffer_h == state_.region.h);
+    const int32_t buffer_stride_bytes = wl_shm_buffer_get_stride(shm_buffer);
 
-  surfaces_.HandleSurfaceFrame(display_number_, buffer_pixels);
+    uint8_t* buffer_pixels =
+        reinterpret_cast<uint8_t*>(wl_shm_buffer_get_data(shm_buffer));
 
-  wl_shm_buffer_end_access(shm_buffer);
+    surfaces_.HandleSurfaceFrame(display_number, buffer_w, buffer_h,
+                                 buffer_stride_bytes, buffer_pixels);
+
+    wl_shm_buffer_end_access(shm_buffer);
+  }
 
   wl_buffer_send_release(state_.current_buffer);
   wl_client_flush(wl_resource_get_client(state_.current_buffer));
@@ -69,4 +74,9 @@
   state_.current_frame_number++;
 }
 
+void Surface::SetVirtioGpuScanoutId(uint32_t scanout_id) {
+  std::unique_lock<std::mutex> lock(state_mutex_);
+  state_.virtio_gpu_metadata_.scanout_id = scanout_id;
+}
+
 }  // namespace wayland
\ No newline at end of file
diff --git a/host/libs/wayland/wayland_surface.h b/host/libs/wayland/wayland_surface.h
index fcf2345..f3b17ae 100644
--- a/host/libs/wayland/wayland_surface.h
+++ b/host/libs/wayland/wayland_surface.h
@@ -19,6 +19,7 @@
 
 #include <stdint.h>
 #include <mutex>
+#include <optional>
 
 #include <wayland-server-core.h>
 
@@ -29,7 +30,7 @@
 // Tracks the buffer associated with a Wayland surface.
 class Surface {
  public:
-  Surface(std::uint32_t display_number, Surfaces& surfaces);
+  Surface(Surfaces& surfaces);
   virtual ~Surface() = default;
 
   Surface(const Surface& rhs) = delete;
@@ -53,10 +54,15 @@
   // Commits the pending frame state.
   void Commit();
 
+  void SetVirtioGpuScanoutId(uint32_t scanout);
+
  private:
-  std::uint32_t display_number_;
   Surfaces& surfaces_;
 
+  struct VirtioGpuMetadata {
+    std::optional<uint32_t> scanout_id;
+  };
+
   struct State {
     uint32_t current_frame_number = 0;
 
@@ -68,6 +74,8 @@
 
     // The buffers expected dimensions.
     Region region;
+
+    VirtioGpuMetadata virtio_gpu_metadata_;
   };
 
   std::mutex state_mutex_;
diff --git a/host/libs/wayland/wayland_surfaces.cpp b/host/libs/wayland/wayland_surfaces.cpp
index 7096574..2fa1ed9 100644
--- a/host/libs/wayland/wayland_surfaces.cpp
+++ b/host/libs/wayland/wayland_surfaces.cpp
@@ -23,42 +23,20 @@
 
 namespace wayland {
 
-Surface* Surfaces::GetOrCreateSurface(std::uint32_t id) {
-  std::unique_lock<std::mutex> lock(surfaces_mutex_);
-
-  auto [it, inserted] = surfaces_.try_emplace(id, nullptr);
-
-  std::unique_ptr<Surface>& surface_ptr = it->second;
-  if (inserted) {
-    surface_ptr.reset(new Surface(id, *this));
-  }
-  return surface_ptr.get();
-}
-
-void Surfaces::OnNextFrame(const FrameCallback& frame_callback) {
-  // Wraps the given callback in a std::package_task that can be waited upon
-  // for completion.
-  Surfaces::FrameCallbackPackaged frame_callback_packaged(
-      [&frame_callback](std::uint32_t display_number,
-                        std::uint8_t* frame_pixels) {
-        frame_callback(display_number, frame_pixels);
-      });
-
-  {
-    std::unique_lock<std::mutex> lock(callback_mutex_);
-    callback_.emplace(&frame_callback_packaged);
-  }
-
-  // Blocks until the frame_callback_packaged was called.
-  frame_callback_packaged.get_future().get();
+void Surfaces::SetFrameCallback(FrameCallback callback) {
+  std::unique_lock<std::mutex> lock(callback_mutex_);
+  callback_.emplace(std::move(callback));
 }
 
 void Surfaces::HandleSurfaceFrame(std::uint32_t display_number,
+                                  std::uint32_t frame_width,
+                                  std::uint32_t frame_height,
+                                  std::uint32_t frame_stride_bytes,
                                   std::uint8_t* frame_bytes) {
   std::unique_lock<std::mutex> lock(callback_mutex_);
   if (callback_) {
-    (*callback_.value())(display_number, frame_bytes);
-    callback_.reset();
+    (callback_.value())(display_number, frame_width, frame_height,
+                        frame_stride_bytes, frame_bytes);
   }
 }
 
diff --git a/host/libs/wayland/wayland_surfaces.h b/host/libs/wayland/wayland_surfaces.h
index ec67ca4..58ed0e8 100644
--- a/host/libs/wayland/wayland_surfaces.h
+++ b/host/libs/wayland/wayland_surfaces.h
@@ -39,27 +39,25 @@
   Surfaces(Surfaces&& rhs) = delete;
   Surfaces& operator=(Surfaces&& rhs) = delete;
 
-  Surface* GetOrCreateSurface(std::uint32_t id);
+  using FrameCallback =
+      std::function<void(std::uint32_t /*display_number*/,      //
+                         std::uint32_t /*frame_width*/,         //
+                         std::uint32_t /*frame_height*/,        //
+                         std::uint32_t /*frame_stride_bytes*/,  //
+                         std::uint8_t* /*frame_bytes*/)>;
 
-  using FrameCallback = std::function<void(std::uint32_t /*display_number*/,
-                                           std::uint8_t* /*frame_pixels*/)>;
-
-  // Blocking
-  void OnNextFrame(const FrameCallback& callback);
+  void SetFrameCallback(FrameCallback callback);
 
  private:
   friend class Surface;
-  void HandleSurfaceFrame(std::uint32_t display_number,
+  void HandleSurfaceFrame(std::uint32_t display_number,      //
+                          std::uint32_t frame_width,         //
+                          std::uint32_t frame_height,        //
+                          std::uint32_t frame_stride_bytes,  //
                           std::uint8_t* frame_bytes);
 
-  std::mutex surfaces_mutex_;
-  std::unordered_map<std::uint32_t, std::unique_ptr<Surface>> surfaces_;
-
-  using FrameCallbackPackaged = std::packaged_task<void(
-      std::uint32_t /*display_number*/, std::uint8_t* /*frame_bytes*/)>;
-
   std::mutex callback_mutex_;
-  std::optional<FrameCallbackPackaged*> callback_;
+  std::optional<FrameCallback> callback_;
 };
 
 }  // namespace wayland
diff --git a/host/libs/wayland/wayland_virtio_gpu_metadata.cpp b/host/libs/wayland/wayland_virtio_gpu_metadata.cpp
new file mode 100644
index 0000000..e26c653
--- /dev/null
+++ b/host/libs/wayland/wayland_virtio_gpu_metadata.cpp
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+#include "host/libs/wayland/wayland_compositor.h"
+
+#include <android-base/logging.h>
+
+#include <virtio-gpu-metadata-v1.h>
+#include <wayland-server-core.h>
+#include <wayland-server-protocol.h>
+
+#include "host/libs/wayland/wayland_surface.h"
+#include "host/libs/wayland/wayland_utils.h"
+
+namespace wayland {
+namespace {
+
+void virtio_gpu_surface_metadata_set_scanout_id(
+    struct wl_client*, struct wl_resource* surface_metadata_resource,
+    uint32_t scanout_id) {
+  GetUserData<Surface>(surface_metadata_resource)
+      ->SetVirtioGpuScanoutId(scanout_id);
+}
+
+const struct wp_virtio_gpu_surface_metadata_v1_interface
+    virtio_gpu_surface_metadata_implementation = {
+        .set_scanout_id = virtio_gpu_surface_metadata_set_scanout_id};
+
+void destroy_virtio_gpu_surface_metadata_resource_callback(
+    struct wl_resource*) {
+  // This is only expected to occur upon surface destruction so no need to
+  // update the scanout id in `Surface`.
+}
+
+void virtio_gpu_metadata_get_surface_metadata(
+    struct wl_client* client, struct wl_resource* /*metadata_impl_resource*/,
+    uint32_t id, struct wl_resource* surface_resource) {
+  Surface* surface = GetUserData<Surface>(surface_resource);
+
+  wl_resource* virtio_gpu_metadata_surface_resource = wl_resource_create(
+      client, &wp_virtio_gpu_surface_metadata_v1_interface, 1, id);
+
+  wl_resource_set_implementation(
+      virtio_gpu_metadata_surface_resource,
+      &virtio_gpu_surface_metadata_implementation, surface,
+      destroy_virtio_gpu_surface_metadata_resource_callback);
+}
+
+const struct wp_virtio_gpu_metadata_v1_interface
+    virtio_gpu_metadata_implementation = {
+        .get_surface_metadata = virtio_gpu_metadata_get_surface_metadata,
+};
+
+void destroy_virtio_gpu_metadata_resource_callback(struct wl_resource*) {}
+
+void bind_virtio_gpu_metadata(wl_client* client, void* data,
+                              uint32_t /*version*/, uint32_t id) {
+  wl_resource* resource =
+      wl_resource_create(client, &wp_virtio_gpu_metadata_v1_interface, 1, id);
+
+  wl_resource_set_implementation(resource, &virtio_gpu_metadata_implementation,
+                                 data,
+                                 destroy_virtio_gpu_metadata_resource_callback);
+}
+
+}  // namespace
+
+void BindVirtioGpuMetadataInterface(wl_display* display, Surfaces* surfaces) {
+  wl_global_create(display, &wp_virtio_gpu_metadata_v1_interface, 1, surfaces,
+                   bind_virtio_gpu_metadata);
+}
+
+}  // namespace wayland
\ No newline at end of file
diff --git a/host/libs/wayland/wayland_virtio_gpu_metadata.h b/host/libs/wayland/wayland_virtio_gpu_metadata.h
new file mode 100644
index 0000000..c414c84
--- /dev/null
+++ b/host/libs/wayland/wayland_virtio_gpu_metadata.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+#include <wayland-server-core.h>
+
+#include "host/libs/wayland/wayland_surfaces.h"
+
+namespace wayland {
+
+// Binds the virtio gpu metadata interface to the given wayland server.
+void BindVirtioGpuMetadataInterface(wl_display* display, Surfaces* surfaces);
+
+}  // namespace wayland
\ No newline at end of file
diff --git a/host/libs/websocket/Android.bp b/host/libs/websocket/Android.bp
index 2a4a522..327224b 100644
--- a/host/libs/websocket/Android.bp
+++ b/host/libs/websocket/Android.bp
@@ -28,6 +28,7 @@
         "liblog",
         "libssl",
         "libcrypto",
+        "libcuttlefish_utils",
     ],
     static_libs: [
         "libcap",
diff --git a/host/libs/websocket/websocket_server.cpp b/host/libs/websocket/websocket_server.cpp
index cdfcba2..40f1357 100644
--- a/host/libs/websocket/websocket_server.cpp
+++ b/host/libs/websocket/websocket_server.cpp
@@ -22,6 +22,7 @@
 #include <android-base/logging.h>
 #include <libwebsockets.h>
 
+#include <common/libs/utils/files.h>
 #include <host/libs/websocket/websocket_handler.h>
 
 namespace cuttlefish {
@@ -32,6 +33,7 @@
     int server_port) {
   std::string cert_file = certs_dir + "/server.crt";
   std::string key_file = certs_dir + "/server.key";
+  std::string ca_file = certs_dir + "/CA.crt";
 
   retry_ = {
       .secs_since_valid_ping = 3,
@@ -63,11 +65,10 @@
   };
 
   struct lws_context_creation_info info;
-  headers_ = {NULL, NULL,
-    "content-security-policy:",
-      "default-src 'self'; "
-      "style-src 'self' https://fonts.googleapis.com/; "
-      "font-src  https://fonts.gstatic.com/; "};
+  headers_ = {NULL, NULL, "content-security-policy:",
+              "default-src 'self' https://ajax.googleapis.com; "
+              "style-src 'self' https://fonts.googleapis.com/; "
+              "font-src  https://fonts.gstatic.com/; "};
 
   memset(&info, 0, sizeof info);
   info.port = server_port;
@@ -79,6 +80,9 @@
   info.options |= LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
   info.ssl_cert_filepath = cert_file.c_str();
   info.ssl_private_key_filepath = key_file.c_str();
+  if (FileExists(ca_file)) {
+    info.ssl_ca_filepath = ca_file.c_str();
+  }
   info.retry_and_idle_policy = &retry_;
 
   context_ = lws_create_context(&info);
diff --git a/shared/BoardConfig.mk b/shared/BoardConfig.mk
index 5f118b0..6be1730 100644
--- a/shared/BoardConfig.mk
+++ b/shared/BoardConfig.mk
@@ -114,9 +114,9 @@
 BOARD_MALLOC_ALIGNMENT := 16
 
 # Disable sparse on all filesystem images
-TARGET_USERIMAGES_SPARSE_EROFS_DISABLED := true
-TARGET_USERIMAGES_SPARSE_EXT_DISABLED := true
-TARGET_USERIMAGES_SPARSE_F2FS_DISABLED := true
+TARGET_USERIMAGES_SPARSE_EROFS_DISABLED ?= true
+TARGET_USERIMAGES_SPARSE_EXT_DISABLED ?= true
+TARGET_USERIMAGES_SPARSE_F2FS_DISABLED ?= true
 
 # Make the userdata partition 6G to accommodate ASAN and CTS
 BOARD_USERDATAIMAGE_PARTITION_SIZE := $(TARGET_USERDATAIMAGE_PARTITION_SIZE)
@@ -159,13 +159,6 @@
 
 BOARD_SEPOLICY_DIRS += system/bt/vendor_libs/linux/sepolicy
 
-# Avoid multiple includes of sepolicy already included by Pixel experience.
-ifneq ($(filter aosp_% %_auto %_go_phone trout_% %_tv,$(PRODUCT_NAME)),)
-
-SYSTEM_EXT_PRIVATE_SEPOLICY_DIRS += hardware/google/pixel-sepolicy/flipendo
-
-endif
-
 # product sepolicy, allow other layers to append
 PRODUCT_PRIVATE_SEPOLICY_DIRS += device/google/cuttlefish/shared/sepolicy/product/private
 # PRODUCT_PUBLIC_SEPOLICY_DIRS += device/google/cuttlefish/shared/sepolicy/product/public
@@ -220,8 +213,6 @@
 # TODO(b/182417593): Move all of these module options to modules.options
 # TODO(b/176860479): Remove once goldfish and cuttlefish share a wifi implementation
 BOARD_BOOTCONFIG += kernel.mac80211_hwsim.radios=0
-# TODO(b/175151042): Remove once we are using virtio-snd on cuttlefish
-BOARD_BOOTCONFIG += kernel.snd-hda-intel.enable=0
 # Reduce slab size usage from virtio vsock to reduce slab fragmentation
 BOARD_BOOTCONFIG += \
     kernel.vmw_vsock_virtio_transport_common.virtio_transport_max_vsock_pkt_buf_size=16384
diff --git a/shared/auto/audio_policy_configuration.xml b/shared/auto/audio_policy_configuration.xml
new file mode 100644
index 0000000..93d4130
--- /dev/null
+++ b/shared/auto/audio_policy_configuration.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2021, 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.
+*/
+-->
+
+<!--
+  Overlay resources to configure car service based on each OEM's preference.
+  See also packages/services/Car/service/res/values/config.xml
+-->
+<audioPolicyConfiguration version="1.0" xmlns:xi="http://www.w3.org/2001/XInclude">
+    <!-- Global configuration Decalaration -->
+    <globalConfiguration speaker_drc_enabled="true"/>
+
+    <module name="primary" halVersion="2.0">
+        <attachedDevices>
+            <item>Speaker</item>
+            <item>Built-In Mic</item>
+        </attachedDevices>
+        <defaultOutputDevice>Speaker</defaultOutputDevice>
+        <mixPorts>
+            <mixPort name="primary output" role="source" flags="AUDIO_OUTPUT_FLAG_PRIMARY">
+                <profile name="" format="AUDIO_FORMAT_PCM_16_BIT"
+                    samplingRates="44100" channelMasks="AUDIO_CHANNEL_OUT_STEREO"/>
+            </mixPort>
+            <mixPort name="primary input" role="sink">
+                <profile name="" format="AUDIO_FORMAT_PCM_16_BIT"
+                    samplingRates="8000,16000" channelMasks="AUDIO_CHANNEL_IN_MONO"/>
+            </mixPort>
+        </mixPorts>
+        <devicePorts>
+            <devicePort tagName="Speaker" role="sink" type="AUDIO_DEVICE_OUT_BUS"
+                address="Speaker">
+                <profile name="" format="AUDIO_FORMAT_PCM_16_BIT"
+                    samplingRates="48000" channelMasks="AUDIO_CHANNEL_OUT_STEREO"/>
+                <gains>
+                    <gain name="" mode="AUDIO_GAIN_MODE_JOINT"
+                        minValueMB="-3200" maxValueMB="600" defaultValueMB="0" stepValueMB="100"/>
+                </gains>
+            </devicePort>
+
+            <devicePort tagName="Built-In Mic" type="AUDIO_DEVICE_IN_BUILTIN_MIC" role="source">
+            </devicePort>
+        </devicePorts>
+        <routes>
+            <route type="mix" sink="Speaker"
+                sources="primary output"/>
+            <route type="mix" sink="primary input"
+                sources="Built-In Mic"/>
+        </routes>
+    </module>
+
+    <xi:include href="audio_policy_volumes.xml"/>
+    <xi:include href="default_volume_tables.xml"/>
+
+    <!-- End of Volume section -->
+    <!-- End of Modules section -->
+
+</audioPolicyConfiguration>
\ No newline at end of file
diff --git a/shared/auto/car_audio_configuration.xml b/shared/auto/car_audio_configuration.xml
new file mode 100644
index 0000000..53ca217
--- /dev/null
+++ b/shared/auto/car_audio_configuration.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+
+<!--
+  Defines the audio configuration in a car, including
+    - Audio zones
+    - Context to audio bus mappings
+    - Volume groups
+  in the car environment.
+-->
+<carAudioConfiguration version="2">
+    <zones>
+        <zone name="primary zone" isPrimary="true">
+            <volumeGroups>
+                <group>
+                    <device address="Speaker">
+                        <context context="music"/>
+                        <context context="navigation"/>
+                        <context context="voice_command"/>
+                        <context context="call_ring"/>
+                        <context context="call"/>
+                        <context context="alarm"/>
+                        <context context="notification"/>
+                        <context context="system_sound"/>
+                        <context context="emergency"/>
+                        <context context="safety"/>
+                        <context context="vehicle_status"/>
+                        <context context="announcement"/>
+                    </device>
+                </group>
+            </volumeGroups>
+        </zone>
+    </zones>
+</carAudioConfiguration>
diff --git a/shared/auto/device.mk b/shared/auto/device.mk
index fb1ccdf..3e6604e 100644
--- a/shared/auto/device.mk
+++ b/shared/auto/device.mk
@@ -45,6 +45,16 @@
     frameworks/native/data/etc/android.software.activities_on_secondary_displays.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.software.activities_on_secondary_displays.xml \
     frameworks/native/data/etc/car_core_hardware.xml:system/etc/permissions/car_core_hardware.xml \
 
+# Preinstalled packages per user type
+PRODUCT_COPY_FILES += \
+    device/google/cuttlefish/shared/auto/preinstalled-packages-product-car-cuttlefish.xml:$(TARGET_COPY_OUT_PRODUCT)/etc/sysconfig/preinstalled-packages-product-car-cuttlefish.xml
+
+LOCAL_AUDIO_PRODUCT_COPY_FILES ?= \
+    device/google/cuttlefish/shared/auto/car_audio_configuration.xml:$(TARGET_COPY_OUT_VENDOR)/etc/car_audio_configuration.xml \
+    device/google/cuttlefish/shared/auto/audio_policy_configuration.xml:$(TARGET_COPY_OUT_VENDOR)/etc/audio_policy_configuration.xml \
+    frameworks/av/services/audiopolicy/config/a2dp_audio_policy_configuration.xml:$(TARGET_COPY_OUT_VENDOR)/etc/a2dp_audio_policy_configuration.xml \
+    frameworks/av/services/audiopolicy/config/usb_audio_policy_configuration.xml:$(TARGET_COPY_OUT_VENDOR)/etc/usb_audio_policy_configuration.xml \
+
 PRODUCT_VENDOR_PROPERTIES += \
     keyguard.no_require_sim=true \
     ro.cdma.home.operator.alpha=Android \
@@ -73,9 +83,13 @@
     canhaldump \
     canhalsend
 
-PRODUCT_PACKAGES += \
-    libcuttlefish-ril-2 \
-    libcuttlefish-rild
+# Cuttlefish RIL support
+TARGET_USES_CF_RILD ?= false
+ifeq ($(TARGET_USES_CF_RILD),true)
+    PRODUCT_PACKAGES += \
+        libcuttlefish-ril-2 \
+        libcuttlefish-rild
+endif
 
 # system_other support
 PRODUCT_PACKAGES += \
diff --git a/shared/auto/manifest.xml b/shared/auto/manifest.xml
index 7bf5d3c..e1691a1 100644
--- a/shared/auto/manifest.xml
+++ b/shared/auto/manifest.xml
@@ -10,39 +10,9 @@
 -->
 <!-- Android Auto Embedded specific HALs-->
 <manifest version="1.0" type="device">
-    <!-- FIXME: Implement automotive.evs HAL
-    <hal format="hidl">
-        <name>android.hardware.automotive.evs</name>
-        <transport>hwbinder</transport>
-        <version>1.0</version>
-        <interface>
-            <name>IEvsEnumerator</name>
-            <instance>default</instance>
-        </interface>
-    </hal>
-    -->
     <hal format="hidl">
         <name>android.hardware.automotive.can</name>
         <transport>hwbinder</transport>
-        <version>1.0</version>
-        <interface>
-            <name>ICanController</name>
-            <instance>socketcan</instance>
-        </interface>
-        <interface>
-            <name>ICanBus</name>
-            <instance>test</instance>
-        </interface>
-    </hal>
-    <!-- FIXME: Move this to shared manifest.xml -->
-    <hal format="hidl">
-        <name>android.hardware.broadcastradio</name>
-        <transport>hwbinder</transport>
-        <version>2.0</version>
-        <interface>
-            <name>IBroadcastRadio</name>
-            <instance>amfm</instance>
-            <instance>dab</instance>
-        </interface>
+        <fqname>@1.0::ICanBus/test</fqname>
     </hal>
 </manifest>
diff --git a/shared/auto/overlay/frameworks/base/core/res/res/values/config.xml b/shared/auto/overlay/frameworks/base/core/res/res/values/config.xml
index 60c7ae9..e42a10f 100644
--- a/shared/auto/overlay/frameworks/base/core/res/res/values/config.xml
+++ b/shared/auto/overlay/frameworks/base/core/res/res/values/config.xml
@@ -16,6 +16,11 @@
 ** limitations under the License.
 */
 -->
+
+<!--
+  Overlay resources to configure car service based on each OEM's preference.
+  See also packages/services/Car/service/res/values/config.xml
+-->
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
   <!-- Enable multi-user. -->
   <bool name="config_enableMultiUserUI" translatable="false">true</bool>
@@ -23,8 +28,14 @@
   <bool name="config_guestUserEphemeral" translatable="false">true</bool>
   <!--  Maximum number of users allowed on the device. -->
   <integer name="config_multiuserMaximumUsers" translatable="false">4</integer>
-  <!-- Restricting eth2 -->
+  <!-- Restricting eth1 -->
   <string-array translatable="false" name="config_ethernet_interfaces">
-    <item>eth2;11,12,14;;</item>
+    <item>eth1;11,12,14;;</item>
   </string-array>
+  <!-- Car uses hardware amplifier for volume. -->
+  <bool name="config_useFixedVolume">true</bool>
+      <!--
+          Handle volume keys directly in CarAudioService without passing them to the foreground app
+      -->
+  <bool name="config_handleVolumeKeysInWindowManager">true</bool>
 </resources>
diff --git a/shared/auto/overlay/packages/services/Car/service/res/values/config.xml b/shared/auto/overlay/packages/services/Car/service/res/values/config.xml
new file mode 100644
index 0000000..6cfd2cc
--- /dev/null
+++ b/shared/auto/overlay/packages/services/Car/service/res/values/config.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2021, 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.
+*/
+-->
+
+<!--
+  Overlay resources to configure car service based on each OEM's preference.
+  See also packages/services/Car/service/res/values/config.xml
+-->
+<resources>
+    <bool name="audioUseDynamicRouting">true</bool>
+    <!--  Configuration to enable muting of individual volume groups. If this is set to
+          false, muting of individual volume groups is disabled, instead muting will toggle master
+          mute. If this is set to true, car volume group muting is enabled and each individual
+          volume group can be muted separately. -->
+    <bool name="audioUseCarVolumeGroupMuting">false</bool>
+    <!--  Configuration to enable IAudioControl#onDevicesToDuckChange API to inform HAL when to
+      duck. If this is set to true, the API will receive signals indicating which output devices
+      to duck as well as what usages are currently holding focus. If set to false, the API will
+      not be called. -->
+    <bool name="audioUseHalDuckingSignals">false</bool>
+
+    <!--
+    Lists all occupant (= driver + passenger) zones available in the car.
+    Some examples are:
+    <item>occupantZoneId=0,occupantType=DRIVER,seatRow=1,seatSide=driver</item>
+    <item>occupantZoneId=1,occupantType=FRONT_PASSENGER,seatRow=1,seatSide=oppositeDriver</item>
+    <item>occupantZoneId=2,occupantType=REAR_PASSENGER,seatRow=2,seatSide=left</item>
+    <item>occupantZoneId=3,occupantType=REAR_PASSENGER,seatRow=2,seatSide=right</item>
+
+    occupantZoneId: Unique unsigned integer id to represent each passenger zone. Each zone
+                    should have different id.
+    occupantType: Occupant type for the display. Use * part from
+                   CarOccupantZoneManager.OCCUPANT_TYPE_* like DRIVER, FRONT_PASSENGER,
+                   REAR_PASSENGER and etc.
+    seatRow: Integer telling which row the seat is located. Row 1 is for front seats.
+    seatSide: left/center/right for known side. Or can use driver/center/oppositeDriver to
+              handle both right-hand driving and left-hand driving in one place.
+              If car's RHD / LHD is not specified, LHD will be assumed and driver side becomes
+              left.
+    -->
+    <string-array translatable="false" name="config_occupant_zones">
+        <item>occupantZoneId=0,occupantType=DRIVER,seatRow=1,seatSide=driver</item>
+    </string-array>
+    <!--
+        Specifies configuration of displays in system telling its usage / type and assigned
+        occupant.
+
+        Some examples are:
+        <item>displayPort=0,displayType=MAIN,occupantZoneId=0</item>
+        <item>displayPort=1,displayType=INSTRUMENT_CLUSTER,occupantZoneId=0</item>
+        <item>displayPort=2,displayType=MAIN,occupantZoneId=1</item>
+        <item>displayPort=3,displayType=MAIN,occupantZoneId=2</item>
+        <item>displayPort=4,displayType=MAIN,occupantZoneId=3</item>
+
+        displayPort: Unique port id for the display.
+        displayType: Display type for the display. Use * part from
+                       CarOccupantZoneManager.DISPLAY_TYPE_* like MAIN, INSTRUMENT_CLUSTER and
+                       etc.
+        occupantZoneId: occupantZoneId specified from config_occupant_zones.
+
+    -->
+    <string-array translatable="false" name="config_occupant_display_mapping">
+        <item>displayPort=0,displayType=MAIN,occupantZoneId=0</item>
+        <item>displayPort=1,displayType=INSTRUMENT_CLUSTER,occupantZoneId=0</item>
+    </string-array>
+
+    <!-- A name of a camera device that provides the rearview through EVS service -->
+    <string translatable="false" name="config_evsRearviewCameraId">/dev/video10</string>
+
+    <!-- The camera Activity name for EVS, if defined, the Activity will be launched by
+         CarEvsService. -->
+    <string name="config_evsCameraActivity" translatable="false">
+        com.google.android.car.evs/com.google.android.car.evs.CarEvsCameraPreviewActivity
+    </string>
+</resources>
diff --git a/vsoc_x86/auto/preinstalled-packages-product-car-cuttlefish.xml b/shared/auto/preinstalled-packages-product-car-cuttlefish.xml
similarity index 96%
rename from vsoc_x86/auto/preinstalled-packages-product-car-cuttlefish.xml
rename to shared/auto/preinstalled-packages-product-car-cuttlefish.xml
index 49aef5d..a156ae8 100644
--- a/vsoc_x86/auto/preinstalled-packages-product-car-cuttlefish.xml
+++ b/shared/auto/preinstalled-packages-product-car-cuttlefish.xml
@@ -30,10 +30,6 @@
         <install-in user-type="FULL" />
         <install-in user-type="SYSTEM" />
     </install-in-user-type>
-    <install-in-user-type package="com.android.managedprovisioning">
-        <install-in user-type="FULL" />
-        <install-in user-type="SYSTEM" />
-    </install-in-user-type>
     <install-in-user-type package="com.android.phone">
         <install-in user-type="FULL" />
         <install-in user-type="SYSTEM" />
@@ -94,8 +90,13 @@
     <install-in-user-type package="com.android.cameraextensions">
         <install-in user-type="SYSTEM" />
         <install-in user-type="FULL" />
+   </install-in-user-type>
+   <install-in-user-type package="com.android.car.messenger">
+	<install-in user-type="FULL" />
+        <install-in user-type="SYSTEM" />
     </install-in-user-type>
 
+
 <!--
   Apps that do need to run on SYSTEM and evaluated by package owner.
   Here the apps will have FULL only.
@@ -136,9 +137,6 @@
     <install-in-user-type package="com.android.car.radio">
         <install-in user-type="FULL" />
     </install-in-user-type>
-    <install-in-user-type package="com.android.car.messenger">
-        <install-in user-type="FULL" />
-    </install-in-user-type>
     <install-in-user-type package="com.android.car.media.localmediaplayer">
         <install-in user-type="FULL" />
     </install-in-user-type>
diff --git a/shared/config/config_foldable.json b/shared/config/config_foldable.json
index dd64d17..a9c7255 100644
--- a/shared/config/config_foldable.json
+++ b/shared/config/config_foldable.json
@@ -1,7 +1,6 @@
 {
-	"x_res" : 1768,
-	"y_res" : 2208,
-	"dpi" : 386,
+	"display0" : "width=1768,height=2208,dpi=374",
+	"display1" : "width=832,height=2268,dpi=387",
 	"memory_mb" : 4096,
 	"custom_actions" : [
                 {
@@ -21,6 +20,19 @@
                         "device_states": [
                                 {
                                         "lid_switch_open": true,
+                                        "hinge_angle_value": 90
+                                }
+                        ],
+                        "button":{
+                                "command":"device_state_half_opened",
+                                "title":"Device State Half-Opened",
+                                "icon_name":"laptop"
+                        }
+                },
+                {
+                        "device_states": [
+                                {
+                                        "lid_switch_open": true,
                                         "hinge_angle_value": 180
                                 }
                         ],
diff --git a/shared/config/init.vendor.rc b/shared/config/init.vendor.rc
index 77b79dc..94ef593 100644
--- a/shared/config/init.vendor.rc
+++ b/shared/config/init.vendor.rc
@@ -49,9 +49,7 @@
 
     start setup_wifi
     # works around framework netiface enumeration issue
-    start rename_eth1
-
-    start bt_vhci_forwarder
+    start rename_eth0
 
     # So GceBootReporter can print to kmsg
     chmod 622 /dev/kmsg
@@ -60,6 +58,8 @@
     # set RLIMIT_MEMLOCK to 64MB
     setrlimit 8 67108864 67108864
 
+    start bt_vhci_forwarder
+
 on post-fs-data
     mkdir /data/vendor/modem_dump 0777 system system
     mkdir /data/vendor/radio 0777 system system
@@ -79,15 +79,16 @@
     mkdir /data/vendor/wifi/wpa 0770 wifi wifi
     mkdir /data/vendor/wifi/wpa/sockets 0770 wifi wifi
     start socket_vsock_proxy
+    setprop ro.hardware.audio.primary goldfish
 
-service bt_vhci_forwarder /vendor/bin/bt_vhci_forwarder -virtio_console_dev=/dev/hvc5
+service bt_vhci_forwarder /vendor/bin/bt_vhci_forwarder -virtio_console_dev=${vendor.ser.bt-uart}
     user bluetooth
     group bluetooth
 
 service setup_wifi /vendor/bin/setup_wifi
     oneshot
 
-service rename_eth1 /vendor/bin/rename_netiface eth1 rmnet0
+service rename_eth0 /vendor/bin/rename_netiface eth0 rmnet0
     oneshot
 
 on property:sys.boot_completed=1
diff --git a/shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen.idc b/shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen_0.idc
similarity index 64%
copy from shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen.idc
copy to shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen_0.idc
index 662f467..a4e2b19 100644
--- a/shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen.idc
+++ b/shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen_0.idc
@@ -3,4 +3,4 @@
 touch.deviceType = touchScreen
 touch.orientationAware = 1
 
-# touch.displayId = local:0
+touch.displayId = local:4619827259835644672
diff --git a/shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen.idc b/shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen_1.idc
similarity index 64%
copy from shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen.idc
copy to shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen_1.idc
index 662f467..a375e58 100644
--- a/shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen.idc
+++ b/shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen_1.idc
@@ -3,4 +3,4 @@
 touch.deviceType = touchScreen
 touch.orientationAware = 1
 
-# touch.displayId = local:0
+touch.displayId = local:4619827551948147201
diff --git a/shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen.idc b/shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen_2.idc
similarity index 64%
rename from shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen.idc
rename to shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen_2.idc
index 662f467..db17d3c 100644
--- a/shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen.idc
+++ b/shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen_2.idc
@@ -3,4 +3,4 @@
 touch.deviceType = touchScreen
 touch.orientationAware = 1
 
-# touch.displayId = local:0
+touch.displayId = local:4619827124781842690
diff --git a/shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen.idc b/shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen_3.idc
similarity index 64%
copy from shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen.idc
copy to shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen_3.idc
index 662f467..b910ad3 100644
--- a/shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen.idc
+++ b/shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen_3.idc
@@ -3,4 +3,4 @@
 touch.deviceType = touchScreen
 touch.orientationAware = 1
 
-# touch.displayId = local:0
+touch.displayId = local:4619827540095559171
diff --git a/shared/config/manifest.xml b/shared/config/manifest.xml
index baca8df..5917647 100644
--- a/shared/config/manifest.xml
+++ b/shared/config/manifest.xml
@@ -18,18 +18,9 @@
 -->
 <manifest version="1.0" type="device" target-level="6">
     <hal format="hidl">
-        <name>android.hardware.audio</name>
-        <transport>hwbinder</transport>
-        <version>6.0</version>
-        <interface>
-            <name>IDevicesFactory</name>
-            <instance>default</instance>
-        </interface>
-    </hal>
-    <hal format="hidl">
         <name>android.hardware.audio.effect</name>
         <transport>hwbinder</transport>
-        <version>6.0</version>
+        <version>7.0</version>
         <interface>
             <name>IEffectsFactory</name>
             <instance>default</instance>
@@ -97,15 +88,6 @@
         </interface>
     </hal>
     -->
-    <hal format="hidl">
-        <name>android.hardware.graphics.composer</name>
-        <transport>hwbinder</transport>
-        <version>2.3</version>
-        <interface>
-            <name>IComposer</name>
-            <instance>default</instance>
-        </interface>
-    </hal>
     <!-- TODO (b/130075874):
     <hal format="hidl">
         <name>android.hardware.ir</name>
@@ -150,31 +132,6 @@
         </interface>
     </hal>
     -->
-    <hal format="hidl">
-        <name>android.hardware.radio</name>
-        <transport>hwbinder</transport>
-        <version>1.6</version>
-        <interface>
-            <name>IRadio</name>
-            <instance>slot1</instance>
-            <!-- cuttlefish doesn't support SIM slot 2/3 -->
-        </interface>
-        <!-- TODO (b/130079344):
-        <interface>
-            <name>ISap</name>
-            <instance>slot1</instance>
-        </interface>
-        -->
-    </hal>
-    <hal format="hidl">
-        <name>android.hardware.radio.config</name>
-        <transport>hwbinder</transport>
-        <version>1.3</version>
-        <interface>
-            <name>IRadioConfig</name>
-            <instance>default</instance>
-        </interface>
-    </hal>
     <!-- TODO (b/130079239):
     <hal format="hidl">
         <name>android.hardware.secure_element</name>
diff --git a/shared/device.mk b/shared/device.mk
index 57dc767..ebade7a 100644
--- a/shared/device.mk
+++ b/shared/device.mk
@@ -27,6 +27,7 @@
 $(call inherit-product, $(SRC_TARGET_DIR)/product/generic_ramdisk.mk)
 
 PRODUCT_SOONG_NAMESPACES += device/generic/goldfish-opengl # for vulkan
+PRODUCT_SOONG_NAMESPACES += device/generic/goldfish # for audio and wifi
 
 PRODUCT_SHIPPING_API_LEVEL := 31
 PRODUCT_USE_DYNAMIC_PARTITIONS := true
@@ -73,10 +74,6 @@
     ro.com.google.locationfeatures=1 \
     persist.sys.fuse.passthrough.enable=true \
 
-# Storage: for factory reset protection feature
-PRODUCT_VENDOR_PROPERTIES += \
-    ro.frp.pst=/dev/block/by-name/frp
-
 # Explanation of specific properties:
 #   debug.hwui.swap_with_damage avoids boot failure on M http://b/25152138
 #   ro.opengles.version OpenGLES 3.0
@@ -98,6 +95,12 @@
     debug.c2.use_dmabufheaps=1 \
     ro.camerax.extensions.enabled=true \
 
+LOCAL_BT_PROPERTIES ?= \
+ vendor.ser.bt-uart?=/dev/hvc5 \
+
+PRODUCT_VENDOR_PROPERTIES += \
+	 ${LOCAL_BT_PROPERTIES} \
+
 # Below is a list of properties we probably should get rid of.
 PRODUCT_VENDOR_PROPERTIES += \
     wlan.driver.status=ok
@@ -299,7 +302,10 @@
     frameworks/native/data/etc/android.software.verified_boot.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.software.verified_boot.xml \
     system/bt/vendor_libs/test_vendor_lib/data/controller_properties.json:vendor/etc/bluetooth/controller_properties.json \
     device/google/cuttlefish/shared/config/task_profiles.json:$(TARGET_COPY_OUT_VENDOR)/etc/task_profiles.json \
-    device/google/cuttlefish/shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen.idc:$(TARGET_COPY_OUT_VENDOR)/usr/idc/Crosvm_Virtio_Multitouch_Touchscreen.idc
+    device/google/cuttlefish/shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen_0.idc:$(TARGET_COPY_OUT_VENDOR)/usr/idc/Crosvm_Virtio_Multitouch_Touchscreen_0.idc \
+    device/google/cuttlefish/shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen_1.idc:$(TARGET_COPY_OUT_VENDOR)/usr/idc/Crosvm_Virtio_Multitouch_Touchscreen_1.idc \
+    device/google/cuttlefish/shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen_2.idc:$(TARGET_COPY_OUT_VENDOR)/usr/idc/Crosvm_Virtio_Multitouch_Touchscreen_2.idc \
+    device/google/cuttlefish/shared/config/input/Crosvm_Virtio_Multitouch_Touchscreen_3.idc:$(TARGET_COPY_OUT_VENDOR)/usr/idc/Crosvm_Virtio_Multitouch_Touchscreen_3.idc
 
 ifeq ($(TARGET_RO_FILE_SYSTEM_TYPE),ext4)
 PRODUCT_COPY_FILES += \
@@ -369,8 +375,7 @@
     hwcomposer.drm_minigbm \
     hwcomposer.cutf \
     hwcomposer-stats \
-    [email protected] \
-    [email protected]
+    [email protected]
 
 #
 # Gralloc HAL
@@ -403,16 +408,17 @@
 # Audio HAL
 #
 LOCAL_AUDIO_PRODUCT_PACKAGE ?= \
-    audio.primary.cutf \
-    audio.r_submix.default \
-    [email protected] \
-    [email protected] \
-    [email protected]
+    android.hardware.audio.service \
+    [email protected] \
+    [email protected] \
 
 LOCAL_AUDIO_PRODUCT_COPY_FILES ?= \
-    device/google/cuttlefish/shared/config/audio_policy.conf:$(TARGET_COPY_OUT_VENDOR)/etc/audio_policy.conf \
-    frameworks/av/services/audiopolicy/config/audio_policy_configuration_generic.xml:$(TARGET_COPY_OUT_VENDOR)/etc/audio_policy_configuration.xml \
-    frameworks/av/services/audiopolicy/config/primary_audio_policy_configuration.xml:$(TARGET_COPY_OUT_VENDOR)/etc/primary_audio_policy_configuration.xml
+    device/generic/goldfish/audio/policy/audio_policy_configuration.xml:$(TARGET_COPY_OUT_VENDOR)/etc/audio_policy_configuration.xml \
+    device/generic/goldfish/audio/policy/primary_audio_policy_configuration.xml:$(TARGET_COPY_OUT_VENDOR)/etc/primary_audio_policy_configuration.xml \
+    frameworks/av/services/audiopolicy/config/r_submix_audio_policy_configuration.xml:$(TARGET_COPY_OUT_VENDOR)/etc/r_submix_audio_policy_configuration.xml \
+    frameworks/av/services/audiopolicy/config/audio_policy_volumes.xml:$(TARGET_COPY_OUT_VENDOR)/etc/audio_policy_volumes.xml \
+    frameworks/av/services/audiopolicy/config/default_volume_tables.xml:$(TARGET_COPY_OUT_VENDOR)/etc/default_volume_tables.xml \
+    frameworks/av/media/libeffects/data/audio_effects.xml:$(TARGET_COPY_OUT_VENDOR)/etc/audio_effects.xml \
 
 LOCAL_AUDIO_DEVICE_PACKAGE_OVERLAYS ?=
 
@@ -468,11 +474,19 @@
 #
 # Camera
 #
+ifeq ($(TARGET_USE_VSOCK_CAMERA_HAL_IMPL),true)
+PRODUCT_PACKAGES += \
+    [email protected] \
+    [email protected]
+DEVICE_MANIFEST_FILE += \
+    device/google/cuttlefish/guest/hals/camera/manifest.xml
+else
 PRODUCT_PACKAGES += \
     [email protected] \
     libgooglecamerahwl_impl \
     [email protected] \
 
+endif
 #
 # Gatekeeper
 #
@@ -485,9 +499,11 @@
 #
 # GPS
 #
-PRODUCT_PACKAGES += \
+LOCAL_GNSS_PRODUCT_PACKAGE ?= \
     android.hardware.gnss-service.example
 
+PRODUCT_PACKAGES += $(LOCAL_GNSS_PRODUCT_PACKAGE)
+
 # Health
 ifeq ($(LOCAL_HEALTH_PRODUCT_PACKAGE),)
     LOCAL_HEALTH_PRODUCT_PACKAGE := \
diff --git a/shared/foldable/display_layout_configuration.xml b/shared/foldable/display_layout_configuration.xml
new file mode 100644
index 0000000..54b76b1
--- /dev/null
+++ b/shared/foldable/display_layout_configuration.xml
@@ -0,0 +1,41 @@
+<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
+<layouts>
+  <layout>
+    <!-- CLOSED: display0 disabled, display1 enabled -->
+    <state>0</state>
+
+    <display enabled="false">
+      <address>4619827259835644672</address>
+    </display>
+
+    <display enabled="true" defaultDisplay="true">
+      <address>4619827551948147201</address>
+    </display>
+  </layout>
+
+  <layout>
+    <!-- HALF_OPENED: display0 enabled, display1 disabled -->
+    <state>1</state>
+
+    <display enabled="true" defaultDisplay="true">
+      <address>4619827259835644672</address>
+    </display>
+
+    <display enabled="false">
+      <address>4619827551948147201</address>
+    </display>
+  </layout>
+
+  <layout>
+    <!-- OPENED: display0 enabled, display1 disabled -->
+    <state>2</state>
+
+    <display enabled="true" defaultDisplay="true">
+      <address>4619827259835644672</address>
+    </display>
+
+    <display enabled="false">
+      <address>4619827551948147201</address>
+    </display>
+  </layout>
+</layouts>
diff --git a/shared/foldable/display_settings.xml b/shared/foldable/display_settings.xml
new file mode 100644
index 0000000..6ad8287
--- /dev/null
+++ b/shared/foldable/display_settings.xml
@@ -0,0 +1,8 @@
+<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
+<display-settings>
+    <config identifier="0" />
+    <!-- Allow rotation of fixed-orientation activities on the inner screen. -->
+    <display
+        name="local:4619827259835644672"
+        ignoreOrientationRequest="true"/>
+</display-settings>
diff --git a/shared/foldable/overlay/frameworks/base/core/res/res/values/config.xml b/shared/foldable/overlay/frameworks/base/core/res/res/values/config.xml
index 2c540e1..918d39a 100644
--- a/shared/foldable/overlay/frameworks/base/core/res/res/values/config.xml
+++ b/shared/foldable/overlay/frameworks/base/core/res/res/values/config.xml
@@ -17,15 +17,12 @@
 */
 -->
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-  <!-- Indicate the display area rect for foldable devices in folded state. -->
-  <!-- left and right bounds come from:  (open_width/2) +/- (folded_width)/2 -->
-  <string name="config_foldedArea">476 0 1292 2208</string>
   <!-- WindowsManager JetPack display features -->
   <string name="config_display_features" translatable="false">fold-[884,0,884,2208]</string>
   <!-- Map of System DeviceState supplied by DeviceStateManager to WM Jetpack posture. -->
   <string-array name="config_device_state_postures" translatable="false">
       <item>0:1</item> <!-- CLOSED : STATE_FLAT -->
-      <item>3:2</item> <!-- HALF_OPENED : STATE_HALF_OPENED -->
+      <item>1:2</item> <!-- HALF_OPENED : STATE_HALF_OPENED -->
       <item>2:3</item> <!-- OPENED : STATE_FLIPPED -->
   </string-array>
   <!-- The device states (supplied by DeviceStateManager) that should be treated as folded by the
@@ -33,6 +30,9 @@
   <integer-array name="config_foldedDeviceStates" translatable="false">
     <item>0</item> <!-- CLOSED -->
   </integer-array>
+  <!-- Indicates whether to enable an animation when unfolding a device or not -->
+  <bool name="config_unfoldTransitionEnabled">true</bool>
+  <bool name="config_supportsConcurrentInternalDisplays">false</bool>
   <!-- Controls whether the device support multi window modes like split-screen. -->
   <bool name="config_supportsMultiWindow">true</bool>
   <!-- Controls whether device supports split-screen mode. -->
diff --git a/shared/pc/overlay/frameworks/base/core/res/res/values/config.xml b/shared/pc/overlay/frameworks/base/core/res/res/values/config.xml
index 86896be..313712f 100644
--- a/shared/pc/overlay/frameworks/base/core/res/res/values/config.xml
+++ b/shared/pc/overlay/frameworks/base/core/res/res/values/config.xml
@@ -20,8 +20,8 @@
   <bool name="config_showNavigationBar" translatable="false">true</bool>
   <!--  Maximum number of supported users -->
   <integer name="config_multiuserMaximumUsers" translatable="false">4</integer>
-  <!-- Restricting eth2 -->
+  <!-- Restricting eth1 -->
   <string-array translatable="false" name="config_ethernet_interfaces">
-    <item>eth2;11,12,14;;</item>
+    <item>eth1;11,12,14;;</item>
   </string-array>
-</resources>
\ No newline at end of file
+</resources>
diff --git a/shared/phone/device.mk b/shared/phone/device.mk
index 4b5f041..faa2c7c 100644
--- a/shared/phone/device.mk
+++ b/shared/phone/device.mk
@@ -45,3 +45,7 @@
 # These flags are important for the GSI, but break auto
 # These are used by aosp_cf_x86_go_phone targets
 PRODUCT_ENFORCE_RRO_TARGETS := framework-res
+
+# Storage: for factory reset protection feature
+PRODUCT_VENDOR_PROPERTIES += \
+    ro.frp.pst=/dev/block/by-name/frp
diff --git a/shared/phone/overlay/frameworks/base/core/res/res/values/config.xml b/shared/phone/overlay/frameworks/base/core/res/res/values/config.xml
index c1b0d3e..61feabf 100644
--- a/shared/phone/overlay/frameworks/base/core/res/res/values/config.xml
+++ b/shared/phone/overlay/frameworks/base/core/res/res/values/config.xml
@@ -55,9 +55,9 @@
   <string name="config_mms_user_agent_profile_url" translatable="false">http://gsm.lge.com/html/gsm/Nexus5-M3.xml</string>
   <string name="config_wlan_data_service_package" translatable="false">com.android.ims</string>
   <string name="config_wlan_network_service_package" translatable="false">com.android.ims</string>
-  <!-- Restricting eth2 -->
+  <!-- Restricting eth1 -->
   <string-array translatable="false" name="config_ethernet_interfaces">
-    <item>eth2;11,12,14;;</item>
+    <item>eth1;11,12,14;;</item>
   </string-array>
 
   <!-- List of biometric sensors on the device, in decreasing strength. Consumed by AuthService
diff --git a/shared/sepolicy/system_ext/private/flipendo.te b/shared/sepolicy/system_ext/private/flipendo.te
deleted file mode 100644
index bdf57dc..0000000
--- a/shared/sepolicy/system_ext/private/flipendo.te
+++ /dev/null
@@ -1 +0,0 @@
-gpu_access(flipendo)
diff --git a/shared/sepolicy/vendor/adbd.te b/shared/sepolicy/vendor/adbd.te
index 4ed653a..d932066 100644
--- a/shared/sepolicy/vendor/adbd.te
+++ b/shared/sepolicy/vendor/adbd.te
@@ -1,9 +1,2 @@
 allow adbd self:{ socket vsock_socket } {create listen accept rw_socket_perms_no_ioctl};
-# TODO(b/130668487): Label the vsock sockets.
-allow adbd unlabeled:{socket vsock_socket} rw_socket_perms_no_ioctl;
 allow adbd kernel:system module_request;
-
-recovery_only(`
-# TODO(b/130668487): Label the vsock sockets.
-allow su unlabeled:{ socket vsock_socket } rw_socket_perms_no_ioctl;
-')
diff --git a/shared/sepolicy/vendor/file_contexts b/shared/sepolicy/vendor/file_contexts
index 20538a5..49ef362 100644
--- a/shared/sepolicy/vendor/file_contexts
+++ b/shared/sepolicy/vendor/file_contexts
@@ -62,6 +62,7 @@
 /vendor/bin/rename_netiface  u:object_r:rename_netiface_exec:s0
 /vendor/bin/suspend_blocker  u:object_r:suspend_blocker_exec:s0
 /vendor/bin/hw/libcuttlefish-rild  u:object_r:libcuttlefish_rild_exec:s0
+/vendor/bin/hw/android\.hardware\.camera\.provider@2\.7-external-vsock-service u:object_r:hal_camera_default_exec:s0
 /vendor/bin/hw/android\.hardware\.camera\.provider@2\.7-service-google u:object_r:hal_camera_default_exec:s0
 /vendor/bin/hw/android\.hardware\.camera\.provider@2\.7-service-google-lazy u:object_r:hal_camera_default_exec:s0
 /vendor/bin/hw/android\.hardware\.power\.stats@1\.0-service\.mock  u:object_r:hal_power_stats_default_exec:s0
@@ -100,6 +101,7 @@
 /vendor/lib(64)?/libglapi.so  u:object_r:same_process_hal_file:s0
 /vendor/lib(64)?/dri/.* u:object_r:same_process_hal_file:s0
 /vendor/lib(64)?/hw/android\.hardware\.graphics\.mapper@4\.0-impl\.minigbm\.so u:object_r:same_process_hal_file:s0
+/vendor/lib(64)?/libminigbm_gralloc.so  u:object_r:same_process_hal_file:s0
 /vendor/lib(64)?/hw/android\.hardware\.health@2\.0-impl-2\.1-cuttlefish\.so  u:object_r:same_process_hal_file:s0
 /vendor/lib(64)?/hw/vulkan.pastel.so  u:object_r:same_process_hal_file:s0
 /vendor/lib(64)?/libcuttlefish_fs.so  u:object_r:same_process_hal_file:s0
diff --git a/shared/sepolicy/vendor/genfs_contexts b/shared/sepolicy/vendor/genfs_contexts
index 22cb59f..3a18743 100644
--- a/shared/sepolicy/vendor/genfs_contexts
+++ b/shared/sepolicy/vendor/genfs_contexts
@@ -23,9 +23,9 @@
 dnl # $2 = rtc number (decimal)
 dnl # $3 = rtc wakeup offset (decimal)
 pushdef(`cf_rtc_wakeup_alarmtimer', `dnl
-genfscon sysfs $1/wakeup/wakeup$3 u:object_r:sysfs_wakeup:s0
+genfscon sysfs $1/wakeup u:object_r:sysfs_wakeup:s0
 genfscon sysfs $1/rtc/rtc$2/wakeup`'eval($3 + 1)`' u:object_r:sysfs_wakeup:s0 # <= 5.5
-genfscon sysfs $1/rtc/rtc$2/alarmtimer.0.auto/wakeup/wakeup`'eval($3 + 1)`' u:object_r:sysfs_wakeup:s0 # >5.5
+genfscon sysfs $1/rtc/rtc$2/alarmtimer.0.auto/wakeup u:object_r:sysfs_wakeup:s0 # >5.5
 dnl')dnl
 dnl
 # crosvm (x86)
@@ -34,9 +34,10 @@
 ## find /sys/devices/platform/* -type d -name 'rtc[0-9]' | sed 's,/rtc[0-9],,'
 genfscon sysfs /devices/platform/rtc_cmos/rtc u:object_r:sysfs_rtc:s0
 ## find /sys/devices/platform/* -type d -name 'wakeup[0-9]'
-cf_rtc_wakeup_alarmtimer(/devices/platform/rtc_cmos, 0, 0)
-genfscon sysfs /devices/platform/rtc-test.1/wakeup/wakeup2 u:object_r:sysfs_wakeup:s0
-genfscon sysfs /devices/platform/rtc-test.2/wakeup/wakeup3 u:object_r:sysfs_wakeup:s0
+genfscon sysfs /devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/wakeup u:object_r:sysfs_wakeup:s0
+cf_rtc_wakeup_alarmtimer(/devices/platform/rtc_cmos, 0, 1)
+## currently disabled
+#genfscon sysfs /devices/LNXSYSTM:00/GFSH0001:00/wakeup u:object_r:sysfs_wakeup:s0
 
 # crosvm (arm64)
 cf_pci_block_device(/devices/platform/10000.pci, 0x6, 4)
@@ -45,8 +46,6 @@
 genfscon sysfs /devices/platform/2000.rtc/rtc u:object_r:sysfs_rtc:s0
 ## find /sys/devices/platform/* -type d -name 'wakeup[0-9]'
 ## arm64 2000.rtc on crosvm does not currently expose a wakeup node
-cf_rtc_wakeup_alarmtimer(/devices/platform/rtc-test.1, 2, 0)
-genfscon sysfs /devices/platform/rtc-test.2/wakeup/wakeup2 u:object_r:sysfs_wakeup:s0
 
 # qemu (x86)
 cf_pci_block_device(/devices/pci0000:00, 0x7, 5)
@@ -54,8 +53,6 @@
 genfscon sysfs /devices/pnp0/00:04/rtc u:object_r:sysfs_rtc:s0
 ## find /sys/devices/platform/* -type d -name 'wakeup[0-9][0-9]'
 cf_rtc_wakeup_alarmtimer(/devices/pnp0/00:04, 0, 19)
-genfscon sysfs /devices/platform/rtc-test.1/wakeup/wakeup21 u:object_r:sysfs_wakeup:s0
-genfscon sysfs /devices/platform/rtc-test.2/wakeup/wakeup22 u:object_r:sysfs_wakeup:s0
 
 # qemu (arm64)
 cf_pci_block_device(/devices/platform/4010000000.pcie/pci0000:00, 0x6, 4)
@@ -74,6 +71,8 @@
 genfscon sysfs /devices/platform/rtc-test.1/rtc u:object_r:sysfs_rtc:s0
 genfscon sysfs /devices/platform/rtc-test.2/rtc u:object_r:sysfs_rtc:s0
 genfscon sysfs /bus/iio/devices u:object_r:sysfs_iio_devices:s0
+cf_rtc_wakeup_alarmtimer(/devices/platform/rtc-test.1, 2, 0)
+genfscon sysfs /devices/platform/rtc-test.2/wakeup u:object_r:sysfs_wakeup:s0
 dnl
 popdef(`cf_pci_block_device')dnl
 popdef(`cf_pci_gpu_device')dnl
diff --git a/shared/sepolicy/vendor/hal_camera_default.te b/shared/sepolicy/vendor/hal_camera_default.te
index 6bf571c..e4dac76 100644
--- a/shared/sepolicy/vendor/hal_camera_default.te
+++ b/shared/sepolicy/vendor/hal_camera_default.te
@@ -10,3 +10,6 @@
 hal_client_domain(hal_camera_default, hal_thermal)
 
 gpu_access(hal_camera_default)
+
+# Vsocket camera
+allow hal_camera_default self:vsock_socket { accept bind create getopt listen read write };
diff --git a/shared/sepolicy/vendor/hal_graphics_composer_default.te b/shared/sepolicy/vendor/hal_graphics_composer_default.te
index 807ec73..2dcee24 100644
--- a/shared/sepolicy/vendor/hal_graphics_composer_default.te
+++ b/shared/sepolicy/vendor/hal_graphics_composer_default.te
@@ -3,6 +3,8 @@
 
 allow hal_graphics_composer_default self:netlink_kobject_uevent_socket { bind create read };
 
+allow hal_graphics_composer_default kmsg_device:chr_file w_file_perms;
+
 # Supress warnings for drm_hwcomposer trying to read some vendor.hwc.*
 # properties as Cuttlefish never configures these properties.
 dontaudit hal_graphics_composer_default default_prop:file read;
\ No newline at end of file
diff --git a/shared/sepolicy/vendor/shell.te b/shared/sepolicy/vendor/shell.te
index cc26032..2ef1897 100644
--- a/shared/sepolicy/vendor/shell.te
+++ b/shared/sepolicy/vendor/shell.te
@@ -1,5 +1,2 @@
 allow shell serial_device:chr_file { getattr ioctl read write };
 allow shell cuttlefish_sensor_injection_exec:file rx_file_perms;
-
-# TODO(b/130668487): Label the vsock sockets.
-allow shell adbd:{ socket vsock_socket } rw_socket_perms_no_ioctl;
diff --git a/shared/tv/overlay/frameworks/base/core/res/res/values/config.xml b/shared/tv/overlay/frameworks/base/core/res/res/values/config.xml
index 5ec4e78..46c64b7 100644
--- a/shared/tv/overlay/frameworks/base/core/res/res/values/config.xml
+++ b/shared/tv/overlay/frameworks/base/core/res/res/values/config.xml
@@ -17,8 +17,8 @@
 <resources>
     <!--  Maximum number of supported users -->
     <integer name="config_multiuserMaximumUsers">4</integer>
-    <!-- Restricting eth2 -->
+    <!-- Restricting eth1 -->
     <string-array translatable="false" name="config_ethernet_interfaces">
-        <item>eth2;11,12,14;;</item>
+        <item>eth1;11,12,14;;</item>
     </string-array>
 </resources>
diff --git a/tests/hal/hal_implementation_test.cpp b/tests/hal/hal_implementation_test.cpp
index e7bdc72..d05f077 100644
--- a/tests/hal/hal_implementation_test.cpp
+++ b/tests/hal/hal_implementation_test.cpp
@@ -34,11 +34,11 @@
     "[email protected]",
     "[email protected]",
     "[email protected]",
-    "[email protected]",
+    "[email protected]",
     "[email protected]",
     "[email protected]",
     "[email protected]",
-    "[email protected]",
+    "[email protected]",
     "[email protected]",
     "[email protected]",
     "[email protected]",
@@ -59,7 +59,6 @@
     "[email protected]",
     "[email protected]",
     "[email protected]",
-    "[email protected]",
     "[email protected]",
     "[email protected]",
     "[email protected]", // converted to AIDL, see b/177470478
diff --git a/vsoc_arm64/kernel.mk b/vsoc_arm64/kernel.mk
index c27f7cb..19d1b7c 100644
--- a/vsoc_arm64/kernel.mk
+++ b/vsoc_arm64/kernel.mk
@@ -14,5 +14,6 @@
 # limitations under the License.
 
 TARGET_KERNEL_USE ?= 5.10
+TARGET_KERNEL_PATH ?= kernel/prebuilts/$(TARGET_KERNEL_USE)/arm64/kernel-$(TARGET_KERNEL_USE)
 
-PRODUCT_COPY_FILES += kernel/prebuilts/$(TARGET_KERNEL_USE)/arm64/kernel-$(TARGET_KERNEL_USE):kernel
+PRODUCT_COPY_FILES += $(TARGET_KERNEL_PATH):kernel
diff --git a/vsoc_arm_only/kernel.mk b/vsoc_arm_only/kernel.mk
index 50b4e0b..f7472e7 100644
--- a/vsoc_arm_only/kernel.mk
+++ b/vsoc_arm_only/kernel.mk
@@ -13,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-PRODUCT_COPY_FILES += device/google/cuttlefish_prebuilts/kernel/5.4-arm/kernel-5.4:kernel
+TARGET_KERNEL_PATH ?= device/google/cuttlefish_prebuilts/kernel/5.4-arm/kernel-5.4
+
+PRODUCT_COPY_FILES += $(TARGET_KERNEL_PATH):kernel
diff --git a/vsoc_x86/auto/device.mk b/vsoc_x86/auto/device.mk
index c3ccba3..e0d5bad8 100644
--- a/vsoc_x86/auto/device.mk
+++ b/vsoc_x86/auto/device.mk
@@ -23,10 +23,6 @@
 PRODUCT_MANUFACTURER := Google
 PRODUCT_MODEL := Cuttlefish x86 auto
 
-# Whitelisted packages per user type
-PRODUCT_COPY_FILES += \
-    device/google/cuttlefish/vsoc_x86/auto/preinstalled-packages-product-car-cuttlefish.xml:$(TARGET_COPY_OUT_PRODUCT)/etc/sysconfig/preinstalled-packages-product-car-cuttlefish.xml
-
 PRODUCT_VENDOR_PROPERTIES += \
     ro.soc.manufacturer=$(PRODUCT_MANUFACTURER) \
     ro.soc.model=$(PRODUCT_DEVICE)
diff --git a/vsoc_x86_64/kernel.mk b/vsoc_x86_64/kernel.mk
index 5dded66..112eb25 100644
--- a/vsoc_x86_64/kernel.mk
+++ b/vsoc_x86_64/kernel.mk
@@ -14,5 +14,6 @@
 # limitations under the License.
 
 TARGET_KERNEL_USE ?= 5.10
+TARGET_KERNEL_PATH ?= kernel/prebuilts/$(TARGET_KERNEL_USE)/x86_64/kernel-$(TARGET_KERNEL_USE)
 
-PRODUCT_COPY_FILES += kernel/prebuilts/$(TARGET_KERNEL_USE)/x86_64/kernel-$(TARGET_KERNEL_USE):kernel
+PRODUCT_COPY_FILES += $(TARGET_KERNEL_PATH):kernel
diff --git a/vsoc_x86_64/phone/aosp_cf_foldable.mk b/vsoc_x86_64/phone/aosp_cf_foldable.mk
index 89b0ea3..c0872d8 100644
--- a/vsoc_x86_64/phone/aosp_cf_foldable.mk
+++ b/vsoc_x86_64/phone/aosp_cf_foldable.mk
@@ -21,7 +21,15 @@
 
 # Include the device state configuration for a foldable device.
 PRODUCT_COPY_FILES += \
-    device/google/cuttlefish/shared/foldable/device_state_configuration.xml:$(TARGET_COPY_OUT_VENDOR)/etc/devicestate/device_state_configuration.xml
+    device/google/cuttlefish/shared/foldable/device_state_configuration.xml:$(TARGET_COPY_OUT_VENDOR)/etc/devicestate/device_state_configuration.xml \
+    device/google/cuttlefish/shared/foldable/display_layout_configuration.xml:$(TARGET_COPY_OUT_VENDOR)/etc/displayconfig/display_layout_configuration.xml \
+    device/google/cuttlefish/shared/foldable/display_settings.xml:$(TARGET_COPY_OUT_VENDOR)/etc/display_settings.xml \
+
+# Sidecar and window extensions enhance multi-display, tablet, and foldable support.
+PRODUCT_PACKAGES += \
+    androidx.window.extensions \
+    androidx.window.sidecar \
+
 # Include RRO settings that specify the fold states and screen information.
 DEVICE_PACKAGE_OVERLAYS += device/google/cuttlefish/shared/foldable/overlay
 # Include the foldable `launch_cvd --config foldable` option.
diff --git a/vsoc_x86_only/kernel.mk b/vsoc_x86_only/kernel.mk
index 23cf086..d06c0a1 100644
--- a/vsoc_x86_only/kernel.mk
+++ b/vsoc_x86_only/kernel.mk
@@ -14,5 +14,6 @@
 # limitations under the License.
 
 TARGET_KERNEL_USE ?= 5.10
+TARGET_KERNEL_PATH ?= device/google/cuttlefish_prebuilts/kernel/$(TARGET_KERNEL_USE)-i686/kernel-$(TARGET_KERNEL_USE)
 
-PRODUCT_COPY_FILES += device/google/cuttlefish_prebuilts/kernel/$(TARGET_KERNEL_USE)-i686/kernel-$(TARGET_KERNEL_USE):kernel
+PRODUCT_COPY_FILES +=$(TARGET_KERNEL_PATH):kernel