diff --git a/src/android/camera_capabilities.cpp b/src/android/camera_capabilities.cpp
index f7a6cda9..3fed3f83 100644
--- a/src/android/camera_capabilities.cpp
+++ b/src/android/camera_capabilities.cpp
@@ -830,12 +830,62 @@ int CameraCapabilities::initializeStaticMetadata()
 	staticMetadata_->addEntry(ANDROID_CONTROL_AE_AVAILABLE_ANTIBANDING_MODES,
 				  aeAvailableAntiBandingModes);
 
-	std::vector<uint8_t> aeAvailableModes = {
-		ANDROID_CONTROL_AE_MODE_ON,
-	};
+	std::vector<uint8_t> aeAvailableModes;
+	/* This will cause problems if one supports only on and the other only off */
+	bool aeAddedOn = false;
+	bool aeAddedOff = false;
+
+	const auto &analogGainModeInfo = controlsInfo.find(&controls::AnalogueGainMode);
+	if (analogGainModeInfo != controlsInfo.end()) {
+		for (const auto &value : analogGainModeInfo->second.values()) {
+			switch (value.get<int32_t>()) {
+			case controls::AnalogueGainModeAuto:
+				if (!aeAddedOn)
+					aeAvailableModes.push_back(ANDROID_CONTROL_AE_MODE_ON);
+				aeAddedOn = true;
+				break;
+			case controls::AnalogueGainModeDisabled:
+				if (!aeAddedOff)
+					aeAvailableModes.push_back(ANDROID_CONTROL_AE_MODE_OFF);
+				aeAddedOff = true;
+				break;
+			default:
+				break;
+			}
+		}
+	}
+
+	const auto &exposureTimeModeInfo = controlsInfo.find(&controls::ExposureTimeMode);
+	if (exposureTimeModeInfo != controlsInfo.end()) {
+		for (const auto &value : exposureTimeModeInfo->second.values()) {
+			switch (value.get<int32_t>()) {
+			case controls::ExposureTimeModeAuto:
+				if (!aeAddedOn)
+					aeAvailableModes.push_back(ANDROID_CONTROL_AE_MODE_ON);
+				aeAddedOn = true;
+				break;
+			case controls::ExposureTimeModeDisabled:
+				if (!aeAddedOff)
+					aeAvailableModes.push_back(ANDROID_CONTROL_AE_MODE_OFF);
+				aeAddedOff = true;
+				break;
+			default:
+				break;
+			}
+		}
+	}
+
+	if (aeAvailableModes.empty())
+		aeAvailableModes.push_back(ANDROID_CONTROL_AE_MODE_ON);
 	staticMetadata_->addEntry(ANDROID_CONTROL_AE_AVAILABLE_MODES,
 				  aeAvailableModes);
 
+	/* In libcamera, turning off AE is equivalient to locking. */
+	uint8_t aeLockAvailable = aeAddedOff ? ANDROID_CONTROL_AE_LOCK_AVAILABLE_TRUE
+					     : ANDROID_CONTROL_AE_LOCK_AVAILABLE_FALSE;
+	staticMetadata_->addEntry(ANDROID_CONTROL_AE_LOCK_AVAILABLE,
+				  aeLockAvailable);
+
 	int64_t minFrameDurationNsec = -1;
 	int64_t maxFrameDurationNsec = -1;
 	const auto frameDurationsInfo = controlsInfo.find(&controls::FrameDurationLimits);
@@ -946,10 +996,6 @@ int CameraCapabilities::initializeStaticMetadata()
 	staticMetadata_->addEntry(ANDROID_CONTROL_SCENE_MODE_OVERRIDES,
 				  sceneModesOverride);
 
-	uint8_t aeLockAvailable = ANDROID_CONTROL_AE_LOCK_AVAILABLE_FALSE;
-	staticMetadata_->addEntry(ANDROID_CONTROL_AE_LOCK_AVAILABLE,
-				  aeLockAvailable);
-
 	uint8_t awbLockAvailable = ANDROID_CONTROL_AWB_LOCK_AVAILABLE_FALSE;
 	staticMetadata_->addEntry(ANDROID_CONTROL_AWB_LOCK_AVAILABLE,
 				  awbLockAvailable);
@@ -1358,6 +1404,9 @@ std::unique_ptr<CameraMetadata> CameraCapabilities::requestTemplateManual() cons
 	if (!manualTemplate)
 		return nullptr;
 
+	uint8_t aeMode = ANDROID_CONTROL_AE_MODE_OFF;
+	manualTemplate->addEntry(ANDROID_CONTROL_AE_MODE, aeMode);
+
 	return manualTemplate;
 }
 
diff --git a/src/android/camera_device.cpp b/src/android/camera_device.cpp
index ef4fbab8..d5027ec5 100644
--- a/src/android/camera_device.cpp
+++ b/src/android/camera_device.cpp
@@ -263,7 +263,9 @@ CameraDevice::Camera3RequestDescriptor::Camera3RequestDescriptor(
 
 CameraDevice::CameraDevice(unsigned int id, std::shared_ptr<Camera> camera)
 	: id_(id), state_(State::Stopped), camera_(std::move(camera)),
-	  facing_(CAMERA_FACING_FRONT), orientation_(0)
+	  facing_(CAMERA_FACING_FRONT), orientation_(0),
+	  aeOn_(true), aeLocked_(false), lastExposureTime_(0),
+	  lastAnalogueGain_(1.0f), lastDigitalGain_(1.0f)
 {
 	camera_->requestCompleted.connect(this, &CameraDevice::requestComplete);
 
@@ -857,6 +859,62 @@ int CameraDevice::processControls(Camera3RequestDescriptor *descriptor)
 		controls.set(controls::draft::TestPatternMode, testPatternMode);
 	}
 
+	/*
+	 * \todo Can we trust that we won't receive values that we didn't
+	 * report supporting?
+	 */
+	if (settings.getEntry(ANDROID_CONTROL_AE_LOCK, &entry))
+		aeLocked_ = *entry.data.u8 != ANDROID_CONTROL_AE_LOCK_OFF;
+
+	if (settings.getEntry(ANDROID_CONTROL_AE_MODE, &entry))
+		aeOn_ = *entry.data.u8 != ANDROID_CONTROL_AE_MODE_OFF;
+
+	AeMode aeMode;
+	if (aeLocked_ && aeOn_)
+		aeMode = AeMode::Lock;
+	else if (aeLocked_ && !aeOn_)
+		aeMode = AeMode::Manual;
+	else if (!aeLocked_ && aeOn_)
+		aeMode = AeMode::Auto;
+	else /* !aeLocked_ && !aeOn_ */
+		aeMode = AeMode::Manual;
+
+	/* Save this so that we can recover it in the result */
+	descriptor->aeMode_ = aeMode;
+
+	const auto &eInfo = camera_->controls().find(&controls::ExposureTimeMode);
+	if (eInfo != camera_->controls().end()) {
+		controls.set(controls::ExposureTimeMode,
+				aeMode == AeMode::Auto ?
+				controls::ExposureTimeModeAuto :
+				controls::ExposureTimeModeDisabled);
+	}
+
+	const auto &gInfo = camera_->controls().find(&controls::AnalogueGainMode);
+	if (gInfo != camera_->controls().end()) {
+		controls.set(controls::AnalogueGainMode,
+				aeMode == AeMode::Auto ?
+				controls::AnalogueGainModeAuto :
+				controls::AnalogueGainModeDisabled);
+	}
+
+	if (settings.getEntry(ANDROID_SENSOR_EXPOSURE_TIME, &entry)) {
+		const auto &info = camera_->controls().find(&controls::ExposureTime);
+		if (info != camera_->controls().end()) {
+			lastExposureTime_ = (*entry.data.i64) / 1000;
+			/* Don't disable libcamera's internal AeMode::Lock */
+			if (aeMode != AeMode::Lock)
+				controls.set(controls::ExposureTime, lastExposureTime_);
+		}
+	}
+
+	/* Trigger libcamera's locked -> manual state change */
+	if (aeMode == AeMode::Manual && !settings.hasEntry(ANDROID_SENSOR_EXPOSURE_TIME)) {
+		const auto &info = camera_->controls().find(&controls::ExposureTime);
+		if (info != camera_->controls().end())
+			controls.set(controls::ExposureTime, lastExposureTime_);
+	}
+
 	return 0;
 }
 
@@ -1345,11 +1403,16 @@ CameraDevice::getResultMetadata(const Camera3RequestDescriptor &descriptor) cons
 	resultMetadata->addEntry(ANDROID_CONTROL_AE_EXPOSURE_COMPENSATION,
 				 value32);
 
-	value = ANDROID_CONTROL_AE_LOCK_OFF;
-	resultMetadata->addEntry(ANDROID_CONTROL_AE_LOCK, value);
+	uint8_t aeLock = (descriptor.aeMode_ == AeMode::Lock)
+		       ? ANDROID_CONTROL_AE_LOCK_ON
+		       : ANDROID_CONTROL_AE_LOCK_OFF;
+	resultMetadata->addEntry(ANDROID_CONTROL_AE_LOCK, aeLock);
 
-	value = ANDROID_CONTROL_AE_MODE_ON;
-	resultMetadata->addEntry(ANDROID_CONTROL_AE_MODE, value);
+	/* Locked means auto + locked in android */
+	uint8_t aeMode = (descriptor.aeMode_ != AeMode::Manual)
+		       ? ANDROID_CONTROL_AE_MODE_ON
+		       : ANDROID_CONTROL_AE_MODE_OFF;
+	resultMetadata->addEntry(ANDROID_CONTROL_AE_MODE, aeMode);
 
 	if (settings.getEntry(ANDROID_CONTROL_AE_TARGET_FPS_RANGE, &entry))
 		/*
@@ -1366,6 +1429,31 @@ CameraDevice::getResultMetadata(const Camera3RequestDescriptor &descriptor) cons
 	resultMetadata->addEntry(ANDROID_CONTROL_AE_PRECAPTURE_TRIGGER, value);
 
 	value = ANDROID_CONTROL_AE_STATE_CONVERGED;
+	if (metadata.contains(controls::AeState)) {
+		switch (metadata.get(controls::AeState)) {
+		case controls::AeStateInactive:
+			value = ANDROID_CONTROL_AE_STATE_INACTIVE;
+			break;
+		case controls::AeStateSearching:
+			value = ANDROID_CONTROL_AE_STATE_SEARCHING;
+			break;
+		case controls::AeStateConverged:
+			value = ANDROID_CONTROL_AE_STATE_CONVERGED;
+			break;
+		case controls::AeStateFlashRequired:
+			value = ANDROID_CONTROL_AE_STATE_FLASH_REQUIRED;
+			break;
+		case controls::AeStatePrecapture:
+			value = ANDROID_CONTROL_AE_STATE_PRECAPTURE;
+			break;
+		default:
+			if (descriptor.aeMode_ == AeMode::Lock) {
+				value = ANDROID_CONTROL_AE_STATE_LOCKED;
+				break;
+			}
+			LOG(HAL, Error) << "Invalid AeState, setting converged";
+		}
+	}
 	resultMetadata->addEntry(ANDROID_CONTROL_AE_STATE, value);
 
 	value = ANDROID_CONTROL_AF_MODE_OFF;
diff --git a/src/android/camera_device.h b/src/android/camera_device.h
index b7d774fe..f693cdbc 100644
--- a/src/android/camera_device.h
+++ b/src/android/camera_device.h
@@ -73,6 +73,12 @@ private:
 
 	CameraDevice(unsigned int id, std::shared_ptr<libcamera::Camera> camera);
 
+	enum class AeMode {
+		Auto,
+		Lock,
+		Manual,
+	};
+
 	struct Camera3RequestDescriptor {
 		enum class Status {
 			Pending,
@@ -96,6 +102,9 @@ private:
 
 		camera3_capture_result_t captureResult_ = {};
 		Status status_ = Status::Pending;
+
+		/* The libcamera internal AE state for this request */
+		AeMode aeMode_ = AeMode::Auto;
 	};
 
 	enum class State {
@@ -146,6 +155,13 @@ private:
 	int facing_;
 	int orientation_;
 
+	/* Track the last-set android AE controls */
+	bool aeOn_;
+	bool aeLocked_;
+	int32_t lastExposureTime_;
+	float lastAnalogueGain_;
+	float lastDigitalGain_;
+
 	CameraMetadata lastSettings_;
 };
 
