diff --git a/src/android/camera_worker.cpp b/src/android/camera_worker.cpp
new file mode 100644
index 000000000000..775473da9f6e
--- /dev/null
+++ b/src/android/camera_worker.cpp
@@ -0,0 +1,88 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ *
+ * camera_worker.cpp - Process capture request on behalf of the Camera HAL
+ */
+
+#include "camera_worker.h"
+
+#include <errno.h>
+#include <sys/poll.h>
+
+#include "camera_device.h"
+
+using namespace libcamera;
+
+void CaptureRequest::addBuffer(Stream *stream, FrameBuffer *buffer, int fence)
+{
+	request_->addBuffer(stream, buffer);
+	acquireFences_.push_back(fence);
+}
+
+void CaptureRequest::queue(Camera *camera)
+{
+	camera->queueRequest(request_);
+}
+
+CameraWorker::CameraWorker(const std::shared_ptr<Camera> &camera)
+	: worker_(camera)
+{
+	worker_.moveToThread(&thread_);
+}
+
+void CameraWorker::start()
+{
+	thread_.start();
+}
+
+void CameraWorker::stop()
+{
+	thread_.exit();
+	thread_.wait();
+}
+
+void CameraWorker::queueRequest(std::unique_ptr<CaptureRequest> request)
+{
+	CaptureRequest *req = request.release();
+	worker_.invokeMethod(&Worker::processRequest, ConnectionTypeQueued, req);
+}
+
+int CameraWorker::Worker::waitFence(int fence)
+{
+	struct pollfd fds = { fence, POLLIN, 0 };
+	constexpr unsigned int timeoutMs = 300;
+	int ret;
+
+	do {
+		ret = poll(&fds, 1, timeoutMs);
+		if (ret == 0)
+			return -ETIME;
+
+		if (ret > 0) {
+			if (fds.revents & (POLLERR | POLLNVAL))
+				return -EINVAL;
+
+			return 0;
+		}
+	} while (ret == -1 && (errno == EINTR || errno == EAGAIN));
+
+	return ret;
+}
+
+void CameraWorker::Worker::processRequest(CaptureRequest *request)
+{
+	for (int fence : request->fences()) {
+		if (fence == -1)
+			continue;
+
+		int ret = waitFence(fence);
+		if (ret < 0)
+			return;
+
+		close(fence);
+	}
+
+	request->queue(camera_.get());
+	delete request;
+}
diff --git a/src/android/camera_worker.h b/src/android/camera_worker.h
new file mode 100644
index 000000000000..7e193513d6f4
--- /dev/null
+++ b/src/android/camera_worker.h
@@ -0,0 +1,73 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ *
+ * camera_worker.h - Process capture request on behalf of the Camera HAL
+ */
+#ifndef __ANDROID_CAMERA_WORKER_H__
+#define __ANDROID_CAMERA_WORKER_H__
+
+#include <memory>
+
+#include <libcamera/buffer.h>
+#include <libcamera/camera.h>
+#include <libcamera/object.h>
+#include <libcamera/request.h>
+#include <libcamera/stream.h>
+
+#include "libcamera/internal/thread.h"
+
+class CameraDevice;
+
+struct CaptureRequest {
+	CaptureRequest(libcamera::Request *request)
+		: request_(request)
+	{
+	}
+
+	const std::vector<int> &fences() const { return acquireFences_; }
+
+	void addBuffer(libcamera::Stream *stream,
+		       libcamera::FrameBuffer *buffer, int fence);
+	void queue(libcamera::Camera *camera);
+
+private:
+	std::vector<int> acquireFences_;
+	libcamera::Request *request_;
+};
+
+class CameraWorker
+{
+public:
+	CameraWorker(const std::shared_ptr<libcamera::Camera> &camera);
+
+	void start();
+	void stop();
+
+	void queueRequest(std::unique_ptr<CaptureRequest> request);
+
+private:
+	class Worker : public libcamera::Object
+	{
+	public:
+		Worker(const std::shared_ptr<libcamera::Camera> &camera)
+			: camera_(camera)
+		{
+		}
+		~Worker()
+		{
+		}
+
+		void processRequest(CaptureRequest *request);
+
+	private:
+		int waitFence(int fence);
+
+		const std::shared_ptr<libcamera::Camera> camera_;
+	};
+
+	Worker worker_;
+	libcamera::Thread thread_;
+};
+
+#endif /* __ANDROID_CAMERA_WORKER_H__ */
diff --git a/src/android/meson.build b/src/android/meson.build
index 802bb89afe57..b2b2293cf62d 100644
--- a/src/android/meson.build
+++ b/src/android/meson.build
@@ -21,6 +21,7 @@ android_hal_sources = files([
     'camera_metadata.cpp',
     'camera_ops.cpp',
     'camera_stream.cpp',
+    'camera_worker.cpp',
     'jpeg/encoder_libjpeg.cpp',
     'jpeg/exif.cpp',
 ])
