diff --git a/src/android/camera_capabilities.cpp b/src/android/camera_capabilities.cpp
index cb5ea5e9..2ce465aa 100644
--- a/src/android/camera_capabilities.cpp
+++ b/src/android/camera_capabilities.cpp
@@ -281,6 +281,8 @@ bool CameraCapabilities::validateManualSensorCapability()
 
 bool CameraCapabilities::validateManualPostProcessingCapability()
 {
+	camera_metadata_ro_entry_t entry;
+
 	const char *noMode = "Manual post processing capability unavailable: ";
 
 	if (!staticMetadata_->entryContains<uint8_t>(ANDROID_CONTROL_AWB_AVAILABLE_MODES,
@@ -307,6 +309,28 @@ bool CameraCapabilities::validateManualPostProcessingCapability()
 		return false;
 	}
 
+	bool found = staticMetadata_->getEntry(ANDROID_TONEMAP_AVAILABLE_TONE_MAP_MODES, &entry);
+	if (!found) {
+		LOG(HAL, Info) << noMode << "missing tonemapping";
+		return false;
+	}
+
+	std::set<uint8_t> tonemapModes;
+	for (unsigned int i = 0; i < entry.count; i++)
+		tonemapModes.insert(entry.data.u8[i]);
+
+	if ((!tonemapModes.count(ANDROID_TONEMAP_MODE_CONTRAST_CURVE) &&
+	     !(tonemapModes.count(ANDROID_TONEMAP_MODE_GAMMA_VALUE) &&
+	       tonemapModes.count(ANDROID_TONEMAP_MODE_PRESET_CURVE))) ||
+	    !tonemapModes.count(ANDROID_TONEMAP_MODE_FAST) ||
+	    !tonemapModes.count(ANDROID_TONEMAP_MODE_HIGH_QUALITY)) {
+		LOG(HAL, Info)
+			<< noMode
+			<< "tonemap modes must contain at least {contrast, fast, hq} "
+			<< "or {gamma, preset, fast, hq}";
+		return false;
+	}
+
 	/*
 	 * \todo return true here after we satisfy all the requirements:
 	 * https://developer.android.com/reference/android/hardware/camera2/CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_MANUAL_POST_PROCESSING
@@ -1165,6 +1189,77 @@ int CameraCapabilities::initializeStaticMetadata()
 		availableResultKeys_.insert(ANDROID_EDGE_MODE);
 	}
 
+	std::vector<uint8_t> availableTonemapModes;
+	bool tonemapGammaSupported = false;
+	bool tonemapPresetSupported = false;
+	const auto tonemapModeInfo = controlsInfo.find(&controls::TonemapMode);
+	if (tonemapModeInfo != controlsInfo.end()) {
+		for (const auto &value : tonemapModeInfo->second.values()) {
+			uint8_t mode;
+			switch (value.get<int32_t>()) {
+			case controls::TonemapModeContrastCurve:
+				mode = ANDROID_TONEMAP_MODE_CONTRAST_CURVE;
+				break;
+			case controls::TonemapModeFast:
+				mode = ANDROID_TONEMAP_MODE_FAST;
+				break;
+			case controls::TonemapModeHighQuality:
+				mode = ANDROID_TONEMAP_MODE_HIGH_QUALITY;
+				break;
+			case controls::TonemapModeGammaValue:
+				mode = ANDROID_TONEMAP_MODE_GAMMA_VALUE;
+				tonemapGammaSupported = true;
+				break;
+			case controls::TonemapModePresetCurve:
+				mode = ANDROID_TONEMAP_MODE_PRESET_CURVE;
+				tonemapPresetSupported = true;
+				break;
+			default:
+				LOG(HAL, Error) << "Unknown tonemap mode";
+				continue;
+			}
+			availableTonemapModes.push_back(mode);
+		}
+	}
+
+	/* \todo Figure out how to report TonemapCurve ControlInfo */
+	const auto tonemapSizeInfo = controlsInfo.find(&controls::TonemapCurveSize);
+	if (!availableTonemapModes.empty() &&
+	    (tonemapSizeInfo != controlsInfo.end())) {
+		/* Available tonemap modes */
+		staticMetadata_->addEntry(ANDROID_TONEMAP_AVAILABLE_TONE_MAP_MODES,
+					  availableTonemapModes);
+		availableCharacteristicsKeys_.insert(ANDROID_TONEMAP_AVAILABLE_TONE_MAP_MODES);
+
+		/* Tonemap size */
+		int32_t tonemapMaxCurvePoints = tonemapSizeInfo->second.max().get<int32_t>();
+		staticMetadata_->addEntry(ANDROID_TONEMAP_MAX_CURVE_POINTS,
+					  tonemapMaxCurvePoints);
+		availableCharacteristicsKeys_.insert(ANDROID_TONEMAP_MAX_CURVE_POINTS);
+
+		/* Tonemap mode */
+		availableRequestKeys_.insert(ANDROID_TONEMAP_MODE);
+		availableResultKeys_.insert(ANDROID_TONEMAP_MODE);
+
+		/* Tonemap curve */
+		availableRequestKeys_.insert(ANDROID_TONEMAP_CURVE_RED);
+		availableRequestKeys_.insert(ANDROID_TONEMAP_CURVE_GREEN);
+		availableRequestKeys_.insert(ANDROID_TONEMAP_CURVE_BLUE);
+		availableResultKeys_.insert(ANDROID_TONEMAP_CURVE_RED);
+		availableResultKeys_.insert(ANDROID_TONEMAP_CURVE_GREEN);
+		availableResultKeys_.insert(ANDROID_TONEMAP_CURVE_BLUE);
+
+		if (tonemapGammaSupported) {
+			availableRequestKeys_.insert(ANDROID_TONEMAP_GAMMA);
+			availableResultKeys_.insert(ANDROID_TONEMAP_GAMMA);
+		}
+
+		if (tonemapPresetSupported) {
+			availableRequestKeys_.insert(ANDROID_TONEMAP_PRESET_CURVE);
+			availableResultKeys_.insert(ANDROID_TONEMAP_PRESET_CURVE);
+		}
+	}
+
 	/* JPEG static metadata. */
 
 	/*
@@ -1894,6 +1989,12 @@ std::unique_ptr<CameraMetadata> CameraCapabilities::requestTemplatePreview() con
 		requestTemplate->addEntry(ANDROID_SHADING_MODE, shadingMode);
 	}
 
+	if (staticMetadata_->entryContains<uint8_t>(ANDROID_TONEMAP_AVAILABLE_TONE_MAP_MODES,
+						    ANDROID_TONEMAP_MODE_FAST)) {
+		uint8_t tonemapMode = ANDROID_TONEMAP_MODE_FAST;
+		requestTemplate->addEntry(ANDROID_TONEMAP_MODE, tonemapMode);
+	}
+
 	return requestTemplate;
 }
 
@@ -1922,6 +2023,12 @@ std::unique_ptr<CameraMetadata> CameraCapabilities::requestTemplateStill() const
 		stillTemplate->appendEntry(ANDROID_NOISE_REDUCTION_MODE, noiseReduction);
 	}
 
+	if (staticMetadata_->entryContains<uint8_t>(ANDROID_TONEMAP_AVAILABLE_TONE_MAP_MODES,
+						    ANDROID_TONEMAP_MODE_HIGH_QUALITY)) {
+		uint8_t tonemapMode = ANDROID_TONEMAP_MODE_HIGH_QUALITY;
+		stillTemplate->appendEntry(ANDROID_TONEMAP_MODE, tonemapMode);
+	}
+
 	return stillTemplate;
 }
 
@@ -1945,6 +2052,12 @@ std::unique_ptr<CameraMetadata> CameraCapabilities::requestTemplateVideo() const
 		previewTemplate->appendEntry(ANDROID_EDGE_MODE, edgeMode);
 	}
 
+	if (staticMetadata_->entryContains<uint8_t>(ANDROID_TONEMAP_AVAILABLE_TONE_MAP_MODES,
+						    ANDROID_TONEMAP_MODE_FAST)) {
+		uint8_t tonemapMode = ANDROID_TONEMAP_MODE_FAST;
+		previewTemplate->appendEntry(ANDROID_TONEMAP_MODE, tonemapMode);
+	}
+
 	/*
 	 * Assume the AE_AVAILABLE_TARGET_FPS_RANGE static metadata
 	 * has been assembled as {{min, max} {max, max}}.
diff --git a/src/android/camera_device.cpp b/src/android/camera_device.cpp
index 7be0ce45..7cffb4b1 100644
--- a/src/android/camera_device.cpp
+++ b/src/android/camera_device.cpp
@@ -1014,6 +1014,95 @@ int CameraDevice::processControls(Camera3RequestDescriptor *descriptor)
 		controls.set(controls::LensShadingMode, shadingMode);
 	}
 
+	if (settings.getEntry(ANDROID_TONEMAP_MODE, &entry)) {
+		const int32_t data = static_cast<int32_t>(*entry.data.u8);
+		int32_t tonemapMode;
+		switch (data) {
+		case ANDROID_TONEMAP_MODE_CONTRAST_CURVE:
+			tonemapMode = controls::TonemapModeContrastCurve;
+			break;
+		case ANDROID_TONEMAP_MODE_FAST:
+			tonemapMode = controls::TonemapModeFast;
+			break;
+		case ANDROID_TONEMAP_MODE_HIGH_QUALITY:
+			tonemapMode = controls::TonemapModeHighQuality;
+			break;
+		case ANDROID_TONEMAP_MODE_GAMMA_VALUE:
+			tonemapMode = controls::TonemapModeGammaValue;
+			break;
+		case ANDROID_TONEMAP_MODE_PRESET_CURVE:
+			tonemapMode = controls::TonemapModePresetCurve;
+			break;
+		default:
+			LOG(HAL, Error)
+				<< "Unknown tonemap mode: " << data;
+			return -EINVAL;
+		}
+
+		controls.set(controls::TonemapMode, tonemapMode);
+	}
+
+	bool curveUpdated = false;
+	tonemapCurve.resize(3);
+	static std::map<camera_metadata_tag, unsigned int> curveTags = {
+		{ ANDROID_TONEMAP_CURVE_RED,   0 },
+		{ ANDROID_TONEMAP_CURVE_GREEN, 1 },
+		{ ANDROID_TONEMAP_CURVE_BLUE,  2 },
+	};
+
+	for (const std::pair<camera_metadata_tag, unsigned int> &tag : curveTags) {
+		if (!settings.getEntry(tag.first, &entry))
+			continue;
+
+		tonemapCurve[tag.second].resize(entry.count);
+		for (unsigned int i = 0; i < entry.count; i++)
+			tonemapCurve[tag.second][i] = *(entry.data.f + i);
+
+		curveUpdated = true;
+	}
+
+	if (curveUpdated) {
+		size_t size = std::max(tonemapCurve[0].size(),
+				       tonemapCurve[1].size());
+		size = std::max(size, tonemapCurve[2].size());
+		controls.set(controls::TonemapCurveSize, size);
+
+		std::vector<float> curve;
+		curve.resize(3 * size);
+		curve.insert(curve.begin(),
+			     tonemapCurve[0].begin(), tonemapCurve[0].end());
+		curve.insert(curve.begin() + size,
+			     tonemapCurve[1].begin(), tonemapCurve[1].end());
+		curve.insert(curve.begin() + 2 * size,
+			     tonemapCurve[2].begin(), tonemapCurve[2].end());
+		controls.set(controls::TonemapCurve, curve);
+	}
+
+	if (settings.getEntry(ANDROID_TONEMAP_GAMMA, &entry)) {
+		const float data = *entry.data.f;
+		controls.set(controls::TonemapGamma, data);
+	}
+
+	if (settings.getEntry(ANDROID_TONEMAP_PRESET_CURVE, &entry)) {
+		const int32_t data = static_cast<int32_t>(*entry.data.u8);
+		int32_t presetCurve;
+		switch (data) {
+		case ANDROID_TONEMAP_PRESET_CURVE_SRGB:
+			presetCurve = controls::TonemapPresetCurveSRGB;
+			break;
+		case ANDROID_TONEMAP_PRESET_CURVE_REC709:
+			presetCurve = controls::TonemapPresetCurveREC709;
+			break;
+		default:
+			LOG(HAL, Error)
+				<< "Unknown tonemap mode: " << data;
+			return -EINVAL;
+		}
+
+		controls.set(controls::TonemapPresetCurve, presetCurve);
+
+	}
+
 	return 0;
 }
 
@@ -1812,6 +1901,79 @@ CameraDevice::getResultMetadata(const Camera3RequestDescriptor &descriptor) cons
 		}
 	}
 
+	if (metadata.contains(controls::TonemapMode)) {
+		bool valid;
+		switch (metadata.get(controls::TonemapMode)) {
+		case controls::TonemapModeContrastCurve:
+			value = ANDROID_TONEMAP_MODE_CONTRAST_CURVE;
+			valid = true;
+			break;
+		case controls::TonemapModeFast:
+			value = ANDROID_TONEMAP_MODE_FAST;
+			valid = true;
+			break;
+		case controls::TonemapModeHighQuality:
+			value = ANDROID_TONEMAP_MODE_HIGH_QUALITY;
+			valid = true;
+			break;
+		case controls::TonemapModeGammaValue:
+			value = ANDROID_TONEMAP_MODE_GAMMA_VALUE;
+			valid = true;
+			break;
+		case controls::TonemapModePresetCurve:
+			value = ANDROID_TONEMAP_MODE_PRESET_CURVE;
+			valid = true;
+			break;
+		default:
+			LOG(HAL, Error) << "Invalid tonemap mode";
+			valid = false;
+		}
+
+		/* Can be null on non-FULL */
+		if (valid)
+			resultMetadata->addEntry(ANDROID_TONEMAP_MODE, value);
+	}
+
+	if (metadata.contains(controls::TonemapCurve) &&
+	    metadata.contains(controls::TonemapCurveSize)) {
+		size_t size = metadata.get(controls::TonemapCurveSize);
+
+		Span<const float> curve = metadata.get(controls::TonemapCurve);
+		Span<const float> red = curve.subspan(0, size);
+		Span<const float> green = curve.subspan(size, size);
+		Span<const float> blue = curve.subspan(2 * size, size);
+
+		resultMetadata->addEntry(ANDROID_TONEMAP_CURVE_RED, red);
+		resultMetadata->addEntry(ANDROID_TONEMAP_CURVE_GREEN, green);
+		resultMetadata->addEntry(ANDROID_TONEMAP_CURVE_BLUE, blue);
+	}
+
+	if (metadata.contains(controls::TonemapGamma)) {
+		float gamma = metadata.get(controls::TonemapGamma);
+		resultMetadata->addEntry(ANDROID_TONEMAP_GAMMA, gamma);
+	}
+
+	if (metadata.contains(controls::TonemapPresetCurve)) {
+		bool valid;
+		switch (metadata.get(controls::TonemapPresetCurve)) {
+		case controls::TonemapPresetCurveSRGB:
+			value = ANDROID_TONEMAP_PRESET_CURVE_SRGB;
+			valid = false;
+			break;
+		case controls::TonemapPresetCurveREC709:
+			value = ANDROID_TONEMAP_PRESET_CURVE_REC709;
+			valid = false;
+			break;
+		default:
+			LOG(HAL, Error) << "Invalid tonemap preset curve";
+			valid = false;
+		}
+
+		/* Can be null on non-FULL */
+		if (valid)
+			resultMetadata->addEntry(ANDROID_TONEMAP_PRESET_CURVE, value);
+	}
+
 	/*
 	 * Return the result metadata pack even is not valid: get() will return
 	 * nullptr.
diff --git a/src/android/camera_device.h b/src/android/camera_device.h
index 01c269d3..2cab11c5 100644
--- a/src/android/camera_device.h
+++ b/src/android/camera_device.h
@@ -140,5 +140,8 @@ private:
 	float lastAnalogueGain_;
 	float lastDigitalGain_;
 
+	/* Build the tonemap curve incrementally */
+	std::vector<std::vector<float>> tonemapCurve;
+
 	CameraMetadata lastSettings_;
 };
