diff --git a/src/layer/inject_controls/inject_controls.cpp b/src/layer/inject_controls/inject_controls.cpp
new file mode 100644
index 000000000000..133a8f51a1f6
--- /dev/null
+++ b/src/layer/inject_controls/inject_controls.cpp
@@ -0,0 +1,164 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2025, Ideas On Board Oy
+ *
+ * Layer implementation for injecting controls
+ */
+
+#include "inject_controls.h"
+
+#include <algorithm>
+#include <set>
+
+#include <libcamera/layer.h>
+
+#include <libcamera/control_ids.h>
+#include <libcamera/controls.h>
+#include <libcamera/request.h>
+
+namespace layer {
+
+namespace inject_controls {
+
+libcamera::ControlInfoMap::Map controls(libcamera::ControlInfoMap &ctrls)
+{
+	auto it = ctrls.find(&libcamera::controls::ExposureTimeMode);
+	if (it != ctrls.end()) {
+		for (auto entry : it->second.values()) {
+			if (entry == libcamera::ControlValue(libcamera::controls::ExposureTimeModeAuto))
+				aeAvailable_ = true;
+			if (entry == libcamera::ControlValue(libcamera::controls::ExposureTimeModeManual))
+				meAvailable_ = true;
+		}
+	}
+
+	it = ctrls.find(&libcamera::controls::AnalogueGainMode);
+	if (it != ctrls.end()) {
+		for (auto entry : it->second.values()) {
+			if (entry == libcamera::ControlValue(libcamera::controls::AnalogueGainModeAuto))
+				agAvailable_ = true;
+			if (entry == libcamera::ControlValue(libcamera::controls::AnalogueGainModeManual))
+				mgAvailable_ = true;
+		}
+	}
+
+	std::set<bool> values;
+	if (aeAvailable_ || agAvailable_)
+		values.insert(true);
+	if (meAvailable_ || mgAvailable_)
+		values.insert(false);
+
+	if (values.empty())
+		return {};
+
+	if (values.size() == 1) {
+		bool value = *values.begin();
+		return { { &libcamera::controls::AeEnable,
+			   libcamera::ControlInfo(value, value, value) } };
+	}
+
+	return { { &libcamera::controls::AeEnable, libcamera::ControlInfo(false, true, true) } };
+}
+
+void queueRequest(libcamera::Request *request)
+{
+	libcamera::ControlList &ctrls = request->controls();
+	auto aeEnable = ctrls.get<bool>(libcamera::controls::AeEnable);
+	if (!aeEnable)
+		return;
+
+	if (*aeEnable) {
+		if (aeAvailable_) {
+			ctrls.set(libcamera::controls::ExposureTimeMode,
+				  libcamera::controls::ExposureTimeModeAuto);
+		}
+
+		if (agAvailable_) {
+			ctrls.set(libcamera::controls::AnalogueGainMode,
+				  libcamera::controls::AnalogueGainModeAuto);
+		}
+	} else {
+		if (meAvailable_) {
+			ctrls.set(libcamera::controls::ExposureTimeMode,
+				  libcamera::controls::ExposureTimeModeManual);
+		}
+
+		if (mgAvailable_) {
+			ctrls.set(libcamera::controls::AnalogueGainMode,
+				  libcamera::controls::AnalogueGainModeManual);
+		}
+	}
+}
+
+void requestCompleted(libcamera::Request *request)
+{
+	libcamera::ControlList &metadata = request->metadata();
+
+	auto eMode = metadata.get<int>(libcamera::controls::ExposureTimeMode);
+	auto aMode = metadata.get<int>(libcamera::controls::AnalogueGainMode);
+
+	if (!eMode && !aMode)
+		return;
+
+	bool ae = eMode && eMode == libcamera::controls::ExposureTimeModeAuto;
+	bool me = eMode && eMode == libcamera::controls::ExposureTimeModeManual;
+	bool ag = aMode && aMode == libcamera::controls::AnalogueGainModeAuto;
+	bool mg = aMode && aMode == libcamera::controls::AnalogueGainModeManual;
+
+	/* Exposure time not reported at all; use gain only */
+	if (!ae && !me) {
+		metadata.set(libcamera::controls::AeEnable, ag);
+		return;
+	}
+
+	/* Analogue gain not reported at all; use exposure time only */
+	if (!ag && !mg) {
+		metadata.set(libcamera::controls::AeEnable, ae);
+		return;
+	}
+
+	/*
+	 * Gain mode and exposure mode are not equal; therefore at least one is
+	 * manual, so set AeEnable to false
+	 */
+	if (ag != ae) {
+		metadata.set(libcamera::controls::AeEnable, false);
+		return;
+	}
+
+	/* ag and ae are equal, so just choose one */
+	metadata.set(libcamera::controls::AeEnable, ag);
+	return;
+}
+
+} /* namespace inject_controls */
+
+} /* namespace layer */
+
+namespace libcamera {
+
+extern "C" {
+
+struct Layer layerInfo {
+	.name = "inject_controls",
+	.layerAPIVersion = 1,
+	.init = nullptr,
+	.bufferCompleted = nullptr,
+	.requestCompleted = layer::inject_controls::requestCompleted,
+	.disconnected = nullptr,
+	.acquire = nullptr,
+	.release = nullptr,
+	.controls = layer::inject_controls::controls,
+	.properties = nullptr,
+	.streams = nullptr,
+	.generateConfiguration = nullptr,
+	.configure = nullptr,
+	.createRequest = nullptr,
+	.queueRequest = layer::inject_controls::queueRequest,
+	.start = nullptr,
+	.stop = nullptr,
+};
+
+}
+
+} /* namespace libcamera */
diff --git a/src/layer/inject_controls/inject_controls.h b/src/layer/inject_controls/inject_controls.h
new file mode 100644
index 000000000000..a65cda897836
--- /dev/null
+++ b/src/layer/inject_controls/inject_controls.h
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2025, Ideas On Board Oy
+ *
+ * Layer implementation for injecting controls
+ */
+
+#pragma once
+
+#include <libcamera/controls.h>
+#include <libcamera/request.h>
+
+namespace layer {
+
+namespace inject_controls {
+
+libcamera::ControlInfoMap::Map controls(libcamera::ControlInfoMap &ctrls);
+void queueRequest(libcamera::Request *);
+void requestCompleted(libcamera::Request *);
+
+bool initialized_ = false;
+bool aeAvailable_ = false;
+bool meAvailable_ = false;
+bool agAvailable_ = false;
+bool mgAvailable_ = false;
+
+} /* namespace inject_controls */
+
+} /* namespace layer */
diff --git a/src/layer/inject_controls/meson.build b/src/layer/inject_controls/meson.build
new file mode 100644
index 000000000000..72f22e184923
--- /dev/null
+++ b/src/layer/inject_controls/meson.build
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: CC0-1.0
+
+layer_name = 'layer_inject_controls'
+
+layer_inject_sources = files([
+    'inject_controls.h',
+    'inject_controls.cpp',
+])
+
+mod = shared_module(layer_name, layer_inject_sources,
+                    name_prefix : '',
+                    include_directories : [layer_includes],
+                    dependencies : libcamera_public,
+                    install : true,
+                    install_dir : layer_install_dir)
diff --git a/src/layer/meson.build b/src/layer/meson.build
index dee5e5ac5804..d5793f8c4525 100644
--- a/src/layer/meson.build
+++ b/src/layer/meson.build
@@ -8,3 +8,5 @@ layer_install_dir = libcamera_libdir / 'layers'
 
 config_h.set('LAYER_DIR',
              '"' + get_option('prefix') / layer_install_dir + '"')
+
+subdir('inject_controls')
