Merge "[sf] fix overflow in snapshot generator" into udc-qpr-dev
diff --git a/Android.bp b/Android.bp
index 3992f82..7f1ef67 100644
--- a/Android.bp
+++ b/Android.bp
@@ -36,6 +36,14 @@
     ],
 }
 
+cc_library_headers {
+    name: "native_headers",
+    host_supported: true,
+    export_include_dirs: [
+        "include/",
+    ],
+}
+
 ndk_headers {
     name: "libandroid_headers",
     from: "include/android",
diff --git a/include/input/InputVerifier.h b/include/input/InputVerifier.h
index d4589f5..3715408 100644
--- a/include/input/InputVerifier.h
+++ b/include/input/InputVerifier.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright 2023 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.
@@ -16,13 +16,25 @@
 
 #pragma once
 
+#include <android-base/result.h>
 #include <input/Input.h>
-#include <map>
+#include "rust/cxx.h"
 
 namespace android {
 
+namespace input {
+namespace verifier {
+struct InputVerifier;
+}
+} // namespace input
+
 /*
  * Crash if the provided touch stream is inconsistent.
+ * This class is a pass-through to the rust implementation of InputVerifier.
+ * The rust class could also be used directly, but it would be less convenient.
+ * We can't directly invoke the rust methods on a rust object. So, there's no way to do:
+ * mVerifier.process_movement(...).
+ * This C++ class makes it a bit easier to use.
  *
  * TODO(b/211379801): Add support for hover events:
  * - No hover move without enter
@@ -34,16 +46,13 @@
 public:
     InputVerifier(const std::string& name);
 
-    void processMovement(int32_t deviceId, int32_t action, uint32_t pointerCount,
-                         const PointerProperties* pointerProperties,
-                         const PointerCoords* pointerCoords, int32_t flags);
+    android::base::Result<void> processMovement(int32_t deviceId, int32_t action,
+                                                uint32_t pointerCount,
+                                                const PointerProperties* pointerProperties,
+                                                const PointerCoords* pointerCoords, int32_t flags);
 
 private:
-    const std::string mName;
-    std::map<int32_t /*deviceId*/, std::bitset<MAX_POINTER_ID + 1>> mTouchingPointerIdsByDevice;
-    void ensureTouchingPointersMatch(int32_t deviceId, uint32_t pointerCount,
-                                     const PointerProperties* pointerProperties,
-                                     const char* action) const;
+    rust::Box<android::input::verifier::InputVerifier> mVerifier;
 };
 
 } // namespace android
diff --git a/libs/gui/Surface.cpp b/libs/gui/Surface.cpp
index ed69100..53a2f64 100644
--- a/libs/gui/Surface.cpp
+++ b/libs/gui/Surface.cpp
@@ -1792,19 +1792,20 @@
 
 int Surface::dispatchSetFrameTimelineInfo(va_list args) {
     ATRACE_CALL();
-    auto frameNumber = static_cast<uint64_t>(va_arg(args, uint64_t));
-    auto frameTimelineVsyncId = static_cast<int64_t>(va_arg(args, int64_t));
-    auto inputEventId = static_cast<int32_t>(va_arg(args, int32_t));
-    auto startTimeNanos = static_cast<int64_t>(va_arg(args, int64_t));
-    auto useForRefreshRateSelection = static_cast<bool>(va_arg(args, int32_t));
-
     ALOGV("Surface::%s", __func__);
+
+    const auto nativeWindowFtlInfo = static_cast<ANativeWindowFrameTimelineInfo>(
+            va_arg(args, ANativeWindowFrameTimelineInfo));
+
     FrameTimelineInfo ftlInfo;
-    ftlInfo.vsyncId = frameTimelineVsyncId;
-    ftlInfo.inputEventId = inputEventId;
-    ftlInfo.startTimeNanos = startTimeNanos;
-    ftlInfo.useForRefreshRateSelection = useForRefreshRateSelection;
-    return setFrameTimelineInfo(frameNumber, ftlInfo);
+    ftlInfo.vsyncId = nativeWindowFtlInfo.frameTimelineVsyncId;
+    ftlInfo.inputEventId = nativeWindowFtlInfo.inputEventId;
+    ftlInfo.startTimeNanos = nativeWindowFtlInfo.startTimeNanos;
+    ftlInfo.useForRefreshRateSelection = nativeWindowFtlInfo.useForRefreshRateSelection;
+    ftlInfo.skippedFrameVsyncId = nativeWindowFtlInfo.skippedFrameVsyncId;
+    ftlInfo.skippedFrameStartTimeNanos = nativeWindowFtlInfo.skippedFrameStartTimeNanos;
+
+    return setFrameTimelineInfo(nativeWindowFtlInfo.frameNumber, ftlInfo);
 }
 
 bool Surface::transformToDisplayInverse() const {
diff --git a/libs/gui/SurfaceComposerClient.cpp b/libs/gui/SurfaceComposerClient.cpp
index 0fda358..5bc05ef 100644
--- a/libs/gui/SurfaceComposerClient.cpp
+++ b/libs/gui/SurfaceComposerClient.cpp
@@ -1027,7 +1027,7 @@
     mEarlyWakeupEnd = false;
     mDesiredPresentTime = 0;
     mIsAutoTimestamp = true;
-    clearFrameTimelineInfo(mFrameTimelineInfo);
+    mFrameTimelineInfo = {};
     mApplyToken = nullptr;
     mMergedTransactionIds.clear();
 }
@@ -2279,27 +2279,13 @@
     if (t.vsyncId != FrameTimelineInfo::INVALID_VSYNC_ID &&
         other.vsyncId != FrameTimelineInfo::INVALID_VSYNC_ID) {
         if (other.vsyncId > t.vsyncId) {
-            t.vsyncId = other.vsyncId;
-            t.inputEventId = other.inputEventId;
-            t.startTimeNanos = other.startTimeNanos;
-            t.useForRefreshRateSelection = other.useForRefreshRateSelection;
+            t = other;
         }
     } else if (t.vsyncId == FrameTimelineInfo::INVALID_VSYNC_ID) {
-        t.vsyncId = other.vsyncId;
-        t.inputEventId = other.inputEventId;
-        t.startTimeNanos = other.startTimeNanos;
-        t.useForRefreshRateSelection = other.useForRefreshRateSelection;
+        t = other;
     }
 }
 
-// copied from FrameTimelineInfo::clear()
-void SurfaceComposerClient::Transaction::clearFrameTimelineInfo(FrameTimelineInfo& t) {
-    t.vsyncId = FrameTimelineInfo::INVALID_VSYNC_ID;
-    t.inputEventId = os::IInputConstants::INVALID_INPUT_EVENT_ID;
-    t.startTimeNanos = 0;
-    t.useForRefreshRateSelection = false;
-}
-
 SurfaceComposerClient::Transaction&
 SurfaceComposerClient::Transaction::setTrustedPresentationCallback(
         const sp<SurfaceControl>& sc, TrustedPresentationCallback cb,
diff --git a/libs/gui/aidl/android/gui/FrameTimelineInfo.aidl b/libs/gui/aidl/android/gui/FrameTimelineInfo.aidl
index 6a86c6a..4b647a4 100644
--- a/libs/gui/aidl/android/gui/FrameTimelineInfo.aidl
+++ b/libs/gui/aidl/android/gui/FrameTimelineInfo.aidl
@@ -37,4 +37,10 @@
     // Whether this vsyncId should be used to heuristically select the display refresh rate
     // TODO(b/281695725): Clean this up once TextureView use setFrameRate API
     boolean useForRefreshRateSelection = false;
+
+    // The VsyncId of a frame that was not drawn and squashed into this frame.
+    long skippedFrameVsyncId = INVALID_VSYNC_ID;
+
+    // The start time of a frame that was not drawn and squashed into this frame.
+    long skippedFrameStartTimeNanos = 0;
 }
diff --git a/libs/gui/include/gui/SurfaceComposerClient.h b/libs/gui/include/gui/SurfaceComposerClient.h
index fb57f63..3cf57b1 100644
--- a/libs/gui/include/gui/SurfaceComposerClient.h
+++ b/libs/gui/include/gui/SurfaceComposerClient.h
@@ -410,7 +410,6 @@
         static sp<IBinder> sApplyToken;
         void releaseBufferIfOverwriting(const layer_state_t& state);
         static void mergeFrameTimelineInfo(FrameTimelineInfo& t, const FrameTimelineInfo& other);
-        static void clearFrameTimelineInfo(FrameTimelineInfo& t);
 
     protected:
         std::unordered_map<sp<IBinder>, ComposerState, IBinderHash> mComposerStates;
diff --git a/libs/input/Android.bp b/libs/input/Android.bp
index 4be7328..80a8c8d 100644
--- a/libs/input/Android.bp
+++ b/libs/input/Android.bp
@@ -33,6 +33,138 @@
     ],
 }
 
+aidl_interface {
+    name: "inputconstants",
+    host_supported: true,
+    vendor_available: true,
+    unstable: true,
+    srcs: [
+        ":inputconstants_aidl",
+    ],
+
+    backend: {
+        rust: {
+            enabled: true,
+        },
+    },
+}
+
+rust_bindgen {
+    name: "libinput_bindgen",
+    host_supported: true,
+    crate_name: "input_bindgen",
+    visibility: ["//frameworks/native/services/inputflinger"],
+    wrapper_src: "InputWrapper.hpp",
+
+    include_dirs: [
+        "frameworks/native/include",
+    ],
+
+    source_stem: "bindings",
+
+    bindgen_flags: [
+        "--verbose",
+        "--allowlist-var=AMOTION_EVENT_FLAG_CANCELED",
+        "--allowlist-var=AMOTION_EVENT_ACTION_CANCEL",
+        "--allowlist-var=AMOTION_EVENT_ACTION_UP",
+        "--allowlist-var=AMOTION_EVENT_ACTION_POINTER_DOWN",
+        "--allowlist-var=AMOTION_EVENT_ACTION_DOWN",
+        "--allowlist-var=AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT",
+        "--allowlist-var=MAX_POINTER_ID",
+    ],
+
+    static_libs: [
+        "inputconstants-cpp",
+        "libui-types",
+    ],
+    shared_libs: ["libc++"],
+    header_libs: [
+        "native_headers",
+        "jni_headers",
+        "flatbuffer_headers",
+    ],
+}
+
+// Contains methods to help access C++ code from rust
+cc_library_static {
+    name: "libinput_from_rust_to_cpp",
+    cpp_std: "c++20",
+    host_supported: true,
+    cflags: [
+        "-Wall",
+        "-Wextra",
+        "-Werror",
+    ],
+    srcs: [
+        "FromRustToCpp.cpp",
+    ],
+
+    generated_headers: [
+        "cxx-bridge-header",
+    ],
+    generated_sources: ["libinput_cxx_bridge_code"],
+
+    shared_libs: [
+        "libbase",
+    ],
+}
+
+genrule {
+    name: "libinput_cxx_bridge_code",
+    tools: ["cxxbridge"],
+    cmd: "$(location cxxbridge) $(in) >> $(out)",
+    srcs: ["input_verifier.rs"],
+    out: ["inputverifier_generated.cpp"],
+}
+
+genrule {
+    name: "libinput_cxx_bridge_header",
+    tools: ["cxxbridge"],
+    cmd: "$(location cxxbridge) $(in) --header >> $(out)",
+    srcs: ["input_verifier.rs"],
+    out: ["input_verifier.rs.h"],
+}
+
+rust_defaults {
+    name: "libinput_rust_defaults",
+    srcs: ["input_verifier.rs"],
+    host_supported: true,
+    rustlibs: [
+        "libbitflags",
+        "libcxx",
+        "libinput_bindgen",
+        "liblogger",
+        "liblog_rust",
+        "inputconstants-rust",
+    ],
+
+    shared_libs: [
+        "libbase",
+        "liblog",
+    ],
+}
+
+rust_ffi_static {
+    name: "libinput_rust",
+    crate_name: "input",
+    defaults: ["libinput_rust_defaults"],
+}
+
+rust_test {
+    name: "libinput_rust_test",
+    defaults: ["libinput_rust_defaults"],
+    whole_static_libs: [
+        "libinput_from_rust_to_cpp",
+    ],
+    test_options: {
+        unit_test: true,
+    },
+    test_suites: ["device_tests"],
+    sanitize: {
+        hwaddress: true,
+    },
+}
+
 cc_library {
     name: "libinput",
     cpp_std: "c++20",
@@ -44,6 +176,7 @@
         "-Wno-unused-parameter",
     ],
     srcs: [
+        "FromRustToCpp.cpp",
         "Input.cpp",
         "InputDevice.cpp",
         "InputEventLabels.cpp",
@@ -70,9 +203,13 @@
     export_header_lib_headers: ["jni_headers"],
 
     generated_headers: [
+        "cxx-bridge-header",
+        "libinput_cxx_bridge_header",
         "toolbox_input_labels",
     ],
 
+    generated_sources: ["libinput_cxx_bridge_code"],
+
     shared_libs: [
         "libbase",
         "libcutils",
@@ -92,20 +229,29 @@
     },
 
     static_libs: [
+        "inputconstants-cpp",
         "libui-types",
         "libtflite_static",
     ],
 
+    whole_static_libs: [
+        "libinput_rust",
+    ],
+
     export_static_lib_headers: [
         "libui-types",
     ],
 
+    export_generated_headers: [
+        "cxx-bridge-header",
+        "libinput_cxx_bridge_header",
+    ],
+
     target: {
         android: {
             srcs: [
                 "InputTransport.cpp",
                 "android/os/IInputFlinger.aidl",
-                ":inputconstants_aidl",
             ],
 
             export_shared_lib_headers: ["libbinder"],
@@ -140,9 +286,6 @@
         host_linux: {
             srcs: [
                 "InputTransport.cpp",
-                "android/os/IInputConstants.aidl",
-                "android/os/IInputFlinger.aidl",
-                "android/os/InputConfig.aidl",
             ],
             static_libs: [
                 "libhostgraphics",
diff --git a/libs/input/FromRustToCpp.cpp b/libs/input/FromRustToCpp.cpp
new file mode 100644
index 0000000..e4ce62e
--- /dev/null
+++ b/libs/input/FromRustToCpp.cpp
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 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 <android-base/logging.h>
+#include <ffi/FromRustToCpp.h>
+
+namespace android {
+
+bool shouldLog(rust::Str tag) {
+    return android::base::ShouldLog(android::base::LogSeverity::DEBUG, tag.data());
+}
+
+} // namespace android
diff --git a/libs/input/InputTransport.cpp b/libs/input/InputTransport.cpp
index f6b4648..4d3d8bc 100644
--- a/libs/input/InputTransport.cpp
+++ b/libs/input/InputTransport.cpp
@@ -4,6 +4,7 @@
 // Provides a shared memory transport for input events.
 //
 #define LOG_TAG "InputTransport"
+#define ATRACE_TAG ATRACE_TAG_INPUT
 
 #include <errno.h>
 #include <fcntl.h>
@@ -13,6 +14,7 @@
 #include <sys/types.h>
 #include <unistd.h>
 
+#include <android-base/logging.h>
 #include <android-base/properties.h>
 #include <android-base/stringprintf.h>
 #include <binder/Parcel.h>
@@ -80,6 +82,7 @@
 
 } // namespace
 
+using android::base::Result;
 using android::base::StringPrintf;
 
 namespace android {
@@ -449,6 +452,13 @@
 
     ALOGD_IF(DEBUG_CHANNEL_MESSAGES, "channel '%s' ~ sent message of type %s", mName.c_str(),
              ftl::enum_string(msg->header.type).c_str());
+
+    if (ATRACE_ENABLED()) {
+        std::string message =
+                StringPrintf("sendMessage(inputChannel=%s, seq=0x%" PRIx32 ", type=0x%" PRIx32 ")",
+                             mName.c_str(), msg->header.seq, msg->header.type);
+        ATRACE_NAME(message.c_str());
+    }
     return OK;
 }
 
@@ -484,6 +494,13 @@
 
     ALOGD_IF(DEBUG_CHANNEL_MESSAGES, "channel '%s' ~ received message of type %s", mName.c_str(),
              ftl::enum_string(msg->header.type).c_str());
+
+    if (ATRACE_ENABLED()) {
+        std::string message = StringPrintf("receiveMessage(inputChannel=%s, seq=0x%" PRIx32
+                                           ", type=0x%" PRIx32 ")",
+                                           mName.c_str(), msg->header.seq, msg->header.type);
+        ATRACE_NAME(message.c_str());
+    }
     return OK;
 }
 
@@ -606,8 +623,12 @@
         ATRACE_NAME(message.c_str());
     }
     if (verifyEvents()) {
-        mInputVerifier.processMovement(deviceId, action, pointerCount, pointerProperties,
-                                       pointerCoords, flags);
+        Result<void> result =
+                mInputVerifier.processMovement(deviceId, action, pointerCount, pointerProperties,
+                                               pointerCoords, flags);
+        if (!result.ok()) {
+            LOG(FATAL) << "Bad stream: " << result.error();
+        }
     }
     if (debugTransportPublisher()) {
         std::string transformString;
diff --git a/libs/input/InputVerifier.cpp b/libs/input/InputVerifier.cpp
index eb75804..32b4ca0 100644
--- a/libs/input/InputVerifier.cpp
+++ b/libs/input/InputVerifier.cpp
@@ -18,111 +18,35 @@
 
 #include <android-base/logging.h>
 #include <input/InputVerifier.h>
+#include "input_verifier.rs.h"
+
+using android::base::Error;
+using android::base::Result;
+using android::input::RustPointerProperties;
 
 namespace android {
 
-/**
- * Log all of the movements that are sent to this verifier. Helps to identify the streams that lead
- * to inconsistent events.
- * Enable this via "adb shell setprop log.tag.InputVerifierLogEvents DEBUG"
- */
-static bool logEvents() {
-    return __android_log_is_loggable(ANDROID_LOG_DEBUG, LOG_TAG "LogEvents", ANDROID_LOG_INFO);
-}
-
 // --- InputVerifier ---
 
-InputVerifier::InputVerifier(const std::string& name) : mName(name){};
+InputVerifier::InputVerifier(const std::string& name)
+      : mVerifier(android::input::verifier::create(name)){};
 
-void InputVerifier::processMovement(int32_t deviceId, int32_t action, uint32_t pointerCount,
-                                    const PointerProperties* pointerProperties,
-                                    const PointerCoords* pointerCoords, int32_t flags) {
-    if (logEvents()) {
-        LOG(ERROR) << "Processing " << MotionEvent::actionToString(action) << " for device "
-                   << deviceId << " (" << pointerCount << " pointer"
-                   << (pointerCount == 1 ? "" : "s") << ") on " << mName;
+Result<void> InputVerifier::processMovement(int32_t deviceId, int32_t action, uint32_t pointerCount,
+                                            const PointerProperties* pointerProperties,
+                                            const PointerCoords* pointerCoords, int32_t flags) {
+    std::vector<RustPointerProperties> rpp;
+    for (size_t i = 0; i < pointerCount; i++) {
+        rpp.emplace_back(RustPointerProperties{.id = pointerProperties[i].id});
     }
-
-    switch (MotionEvent::getActionMasked(action)) {
-        case AMOTION_EVENT_ACTION_DOWN: {
-            auto [it, inserted] = mTouchingPointerIdsByDevice.insert({deviceId, {}});
-            if (!inserted) {
-                LOG(FATAL) << "Got ACTION_DOWN, but already have touching pointers " << it->second
-                           << " for device " << deviceId << " on " << mName;
-            }
-            it->second.set(pointerProperties[0].id);
-            break;
-        }
-        case AMOTION_EVENT_ACTION_POINTER_DOWN: {
-            auto it = mTouchingPointerIdsByDevice.find(deviceId);
-            if (it == mTouchingPointerIdsByDevice.end()) {
-                LOG(FATAL) << "Got POINTER_DOWN, but no touching pointers for device " << deviceId
-                           << " on " << mName;
-            }
-            it->second.set(pointerProperties[MotionEvent::getActionIndex(action)].id);
-            break;
-        }
-        case AMOTION_EVENT_ACTION_MOVE: {
-            ensureTouchingPointersMatch(deviceId, pointerCount, pointerProperties, "MOVE");
-            break;
-        }
-        case AMOTION_EVENT_ACTION_POINTER_UP: {
-            auto it = mTouchingPointerIdsByDevice.find(deviceId);
-            if (it == mTouchingPointerIdsByDevice.end()) {
-                LOG(FATAL) << "Got POINTER_UP, but no touching pointers for device " << deviceId
-                           << " on " << mName;
-            }
-            it->second.reset(pointerProperties[MotionEvent::getActionIndex(action)].id);
-            break;
-        }
-        case AMOTION_EVENT_ACTION_UP: {
-            auto it = mTouchingPointerIdsByDevice.find(deviceId);
-            if (it == mTouchingPointerIdsByDevice.end()) {
-                LOG(FATAL) << "Got ACTION_UP, but no record for deviceId " << deviceId << " on "
-                           << mName;
-            }
-            const auto& [_, touchingPointerIds] = *it;
-            if (touchingPointerIds.count() != 1) {
-                LOG(FATAL) << "Got ACTION_UP, but we have pointers: " << touchingPointerIds
-                           << " for deviceId " << deviceId << " on " << mName;
-            }
-            const int32_t pointerId = pointerProperties[0].id;
-            if (!touchingPointerIds.test(pointerId)) {
-                LOG(FATAL) << "Got ACTION_UP, but pointerId " << pointerId
-                           << " is not touching. Touching pointers: " << touchingPointerIds
-                           << " for deviceId " << deviceId << " on " << mName;
-            }
-            mTouchingPointerIdsByDevice.erase(it);
-            break;
-        }
-        case AMOTION_EVENT_ACTION_CANCEL: {
-            if ((flags & AMOTION_EVENT_FLAG_CANCELED) != AMOTION_EVENT_FLAG_CANCELED) {
-                LOG(FATAL) << "For ACTION_CANCEL, must set FLAG_CANCELED";
-            }
-            ensureTouchingPointersMatch(deviceId, pointerCount, pointerProperties, "CANCEL");
-            mTouchingPointerIdsByDevice.erase(deviceId);
-            break;
-        }
+    rust::Slice<const RustPointerProperties> properties{rpp.data(), rpp.size()};
+    rust::String errorMessage =
+            android::input::verifier::process_movement(*mVerifier, deviceId, action, properties,
+                                                       flags);
+    if (errorMessage.empty()) {
+        return {};
+    } else {
+        return Error() << errorMessage;
     }
 }
 
-void InputVerifier::ensureTouchingPointersMatch(int32_t deviceId, uint32_t pointerCount,
-                                                const PointerProperties* pointerProperties,
-                                                const char* action) const {
-    auto it = mTouchingPointerIdsByDevice.find(deviceId);
-    if (it == mTouchingPointerIdsByDevice.end()) {
-        LOG(FATAL) << "Got " << action << ", but no touching pointers for device " << deviceId
-                   << " on " << mName;
-    }
-    const auto& [_, touchingPointerIds] = *it;
-    for (size_t i = 0; i < pointerCount; i++) {
-        const int32_t pointerId = pointerProperties[i].id;
-        if (!touchingPointerIds.test(pointerId)) {
-            LOG(FATAL) << "Got " << action << " for pointerId " << pointerId
-                       << " but the touching pointers are " << touchingPointerIds << " on "
-                       << mName;
-        }
-    }
-};
-
 } // namespace android
diff --git a/libs/input/InputWrapper.hpp b/libs/input/InputWrapper.hpp
new file mode 100644
index 0000000..a01080d
--- /dev/null
+++ b/libs/input/InputWrapper.hpp
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2023 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 <android/input.h>
+#include "input/Input.h"
diff --git a/libs/input/ffi/FromRustToCpp.h b/libs/input/ffi/FromRustToCpp.h
new file mode 100644
index 0000000..889945c
--- /dev/null
+++ b/libs/input/ffi/FromRustToCpp.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2023 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 "rust/cxx.h"
+
+namespace android {
+
+bool shouldLog(rust::Str tag);
+
+} // namespace android
diff --git a/libs/input/input_verifier.rs b/libs/input/input_verifier.rs
new file mode 100644
index 0000000..2e05a63
--- /dev/null
+++ b/libs/input/input_verifier.rs
@@ -0,0 +1,421 @@
+/*
+ * Copyright 2023 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.
+ */
+
+//! Validate the incoming motion stream.
+//! This class is not thread-safe.
+//! State is stored in the "InputVerifier" object
+//! that can be created via the 'create' method.
+//! Usage:
+//! Box<InputVerifier> verifier = create("inputChannel name");
+//! result = process_movement(verifier, ...);
+//! if (result) {
+//!    crash(result.error_message());
+//! }
+
+use std::collections::HashMap;
+use std::collections::HashSet;
+
+use bitflags::bitflags;
+use log::info;
+
+#[cxx::bridge(namespace = "android::input")]
+mod ffi {
+    #[namespace = "android"]
+    unsafe extern "C++" {
+        include!("ffi/FromRustToCpp.h");
+        fn shouldLog(tag: &str) -> bool;
+    }
+    #[namespace = "android::input::verifier"]
+    extern "Rust" {
+        type InputVerifier;
+
+        fn create(name: String) -> Box<InputVerifier>;
+        fn process_movement(
+            verifier: &mut InputVerifier,
+            device_id: i32,
+            action: u32,
+            pointer_properties: &[RustPointerProperties],
+            flags: i32,
+        ) -> String;
+    }
+
+    pub struct RustPointerProperties {
+        id: i32,
+    }
+}
+
+use crate::ffi::shouldLog;
+use crate::ffi::RustPointerProperties;
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+struct DeviceId(i32);
+
+fn process_movement(
+    verifier: &mut InputVerifier,
+    device_id: i32,
+    action: u32,
+    pointer_properties: &[RustPointerProperties],
+    flags: i32,
+) -> String {
+    let result = verifier.process_movement(
+        DeviceId(device_id),
+        action,
+        pointer_properties,
+        Flags::from_bits(flags).unwrap(),
+    );
+    match result {
+        Ok(()) => "".to_string(),
+        Err(e) => e,
+    }
+}
+
+fn create(name: String) -> Box<InputVerifier> {
+    Box::new(InputVerifier::new(&name))
+}
+
+#[repr(u32)]
+enum MotionAction {
+    Down = input_bindgen::AMOTION_EVENT_ACTION_DOWN,
+    Up = input_bindgen::AMOTION_EVENT_ACTION_UP,
+    Move = input_bindgen::AMOTION_EVENT_ACTION_MOVE,
+    Cancel = input_bindgen::AMOTION_EVENT_ACTION_CANCEL,
+    Outside = input_bindgen::AMOTION_EVENT_ACTION_OUTSIDE,
+    PointerDown { action_index: usize } = input_bindgen::AMOTION_EVENT_ACTION_POINTER_DOWN,
+    PointerUp { action_index: usize } = input_bindgen::AMOTION_EVENT_ACTION_POINTER_UP,
+    HoverEnter = input_bindgen::AMOTION_EVENT_ACTION_HOVER_ENTER,
+    HoverMove = input_bindgen::AMOTION_EVENT_ACTION_HOVER_MOVE,
+    HoverExit = input_bindgen::AMOTION_EVENT_ACTION_HOVER_EXIT,
+    Scroll = input_bindgen::AMOTION_EVENT_ACTION_SCROLL,
+    ButtonPress = input_bindgen::AMOTION_EVENT_ACTION_BUTTON_PRESS,
+    ButtonRelease = input_bindgen::AMOTION_EVENT_ACTION_BUTTON_RELEASE,
+}
+
+fn get_action_index(action: u32) -> usize {
+    let index = (action & input_bindgen::AMOTION_EVENT_ACTION_POINTER_INDEX_MASK)
+        >> input_bindgen::AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
+    index.try_into().unwrap()
+}
+
+impl From<u32> for MotionAction {
+    fn from(action: u32) -> Self {
+        let action_masked = action & input_bindgen::AMOTION_EVENT_ACTION_MASK;
+        let action_index = get_action_index(action);
+        match action_masked {
+            input_bindgen::AMOTION_EVENT_ACTION_DOWN => MotionAction::Down,
+            input_bindgen::AMOTION_EVENT_ACTION_UP => MotionAction::Up,
+            input_bindgen::AMOTION_EVENT_ACTION_MOVE => MotionAction::Move,
+            input_bindgen::AMOTION_EVENT_ACTION_CANCEL => MotionAction::Cancel,
+            input_bindgen::AMOTION_EVENT_ACTION_OUTSIDE => MotionAction::Outside,
+            input_bindgen::AMOTION_EVENT_ACTION_POINTER_DOWN => {
+                MotionAction::PointerDown { action_index }
+            }
+            input_bindgen::AMOTION_EVENT_ACTION_POINTER_UP => {
+                MotionAction::PointerUp { action_index }
+            }
+            input_bindgen::AMOTION_EVENT_ACTION_HOVER_ENTER => MotionAction::HoverEnter,
+            input_bindgen::AMOTION_EVENT_ACTION_HOVER_MOVE => MotionAction::HoverMove,
+            input_bindgen::AMOTION_EVENT_ACTION_HOVER_EXIT => MotionAction::HoverExit,
+            input_bindgen::AMOTION_EVENT_ACTION_SCROLL => MotionAction::Scroll,
+            input_bindgen::AMOTION_EVENT_ACTION_BUTTON_PRESS => MotionAction::ButtonPress,
+            input_bindgen::AMOTION_EVENT_ACTION_BUTTON_RELEASE => MotionAction::ButtonRelease,
+            _ => panic!("Unknown action: {}", action),
+        }
+    }
+}
+
+bitflags! {
+    struct Flags: i32 {
+        const CANCELED = input_bindgen::AMOTION_EVENT_FLAG_CANCELED;
+    }
+}
+
+fn motion_action_to_string(action: u32) -> String {
+    match action.into() {
+        MotionAction::Down => "DOWN".to_string(),
+        MotionAction::Up => "UP".to_string(),
+        MotionAction::Move => "MOVE".to_string(),
+        MotionAction::Cancel => "CANCEL".to_string(),
+        MotionAction::Outside => "OUTSIDE".to_string(),
+        MotionAction::PointerDown { action_index } => {
+            format!("POINTER_DOWN({})", action_index)
+        }
+        MotionAction::PointerUp { action_index } => {
+            format!("POINTER_UP({})", action_index)
+        }
+        MotionAction::HoverMove => "HOVER_MOVE".to_string(),
+        MotionAction::Scroll => "SCROLL".to_string(),
+        MotionAction::HoverEnter => "HOVER_ENTER".to_string(),
+        MotionAction::HoverExit => "HOVER_EXIT".to_string(),
+        MotionAction::ButtonPress => "BUTTON_PRESS".to_string(),
+        MotionAction::ButtonRelease => "BUTTON_RELEASE".to_string(),
+    }
+}
+
+/**
+ * Log all of the movements that are sent to this verifier. Helps to identify the streams that lead
+ * to inconsistent events.
+ * Enable this via "adb shell setprop log.tag.InputVerifierLogEvents DEBUG"
+ */
+fn log_events() -> bool {
+    shouldLog("InputVerifierLogEvents")
+}
+
+struct InputVerifier {
+    name: String,
+    touching_pointer_ids_by_device: HashMap<DeviceId, HashSet<i32>>,
+}
+
+impl InputVerifier {
+    fn new(name: &str) -> Self {
+        logger::init(
+            logger::Config::default()
+                .with_tag_on_device("InputVerifier")
+                .with_min_level(log::Level::Trace),
+        );
+        Self { name: name.to_owned(), touching_pointer_ids_by_device: HashMap::new() }
+    }
+
+    fn process_movement(
+        &mut self,
+        device_id: DeviceId,
+        action: u32,
+        pointer_properties: &[RustPointerProperties],
+        flags: Flags,
+    ) -> Result<(), String> {
+        if log_events() {
+            info!(
+                "Processing {} for device {:?} ({} pointer{}) on {}",
+                motion_action_to_string(action),
+                device_id,
+                pointer_properties.len(),
+                if pointer_properties.len() == 1 { "" } else { "s" },
+                self.name
+            );
+        }
+
+        match action.into() {
+            MotionAction::Down => {
+                let it = self
+                    .touching_pointer_ids_by_device
+                    .entry(device_id)
+                    .or_insert_with(HashSet::new);
+                let pointer_id = pointer_properties[0].id;
+                if it.contains(&pointer_id) {
+                    return Err(format!(
+                        "{}: Invalid DOWN event - pointers already down for device {:?}: {:?}",
+                        self.name, device_id, it
+                    ));
+                }
+                it.insert(pointer_id);
+            }
+            MotionAction::PointerDown { action_index } => {
+                if !self.touching_pointer_ids_by_device.contains_key(&device_id) {
+                    return Err(format!(
+                        "{}: Received POINTER_DOWN but no pointers are currently down \
+                        for device {:?}",
+                        self.name, device_id
+                    ));
+                }
+                let it = self.touching_pointer_ids_by_device.get_mut(&device_id).unwrap();
+                let pointer_id = pointer_properties[action_index].id;
+                if it.contains(&pointer_id) {
+                    return Err(format!(
+                        "{}: Pointer with id={} not found in the properties",
+                        self.name, pointer_id
+                    ));
+                }
+                it.insert(pointer_id);
+            }
+            MotionAction::Move => {
+                if !self.ensure_touching_pointers_match(device_id, pointer_properties) {
+                    return Err(format!(
+                        "{}: ACTION_MOVE touching pointers don't match",
+                        self.name
+                    ));
+                }
+            }
+            MotionAction::PointerUp { action_index } => {
+                if !self.touching_pointer_ids_by_device.contains_key(&device_id) {
+                    return Err(format!(
+                        "{}: Received POINTER_UP but no pointers are currently down for device \
+                        {:?}",
+                        self.name, device_id
+                    ));
+                }
+                let it = self.touching_pointer_ids_by_device.get_mut(&device_id).unwrap();
+                let pointer_id = pointer_properties[action_index].id;
+                it.remove(&pointer_id);
+            }
+            MotionAction::Up => {
+                if !self.touching_pointer_ids_by_device.contains_key(&device_id) {
+                    return Err(format!(
+                        "{} Received ACTION_UP but no pointers are currently down for device {:?}",
+                        self.name, device_id
+                    ));
+                }
+                let it = self.touching_pointer_ids_by_device.get_mut(&device_id).unwrap();
+                if it.len() != 1 {
+                    return Err(format!(
+                        "{}: Got ACTION_UP, but we have pointers: {:?} for device {:?}",
+                        self.name, it, device_id
+                    ));
+                }
+                let pointer_id = pointer_properties[0].id;
+                if !it.contains(&pointer_id) {
+                    return Err(format!(
+                        "{}: Got ACTION_UP, but pointerId {} is not touching. Touching pointers:\
+                        {:?} for device {:?}",
+                        self.name, pointer_id, it, device_id
+                    ));
+                }
+                it.clear();
+            }
+            MotionAction::Cancel => {
+                if flags.contains(Flags::CANCELED) {
+                    return Err(format!(
+                        "{}: For ACTION_CANCEL, must set FLAG_CANCELED",
+                        self.name
+                    ));
+                }
+                if !self.ensure_touching_pointers_match(device_id, pointer_properties) {
+                    return Err(format!(
+                        "{}: Got ACTION_CANCEL, but the pointers don't match. \
+                        Existing pointers: {:?}",
+                        self.name, self.touching_pointer_ids_by_device
+                    ));
+                }
+                self.touching_pointer_ids_by_device.remove(&device_id);
+            }
+            _ => return Ok(()),
+        }
+        Ok(())
+    }
+
+    fn ensure_touching_pointers_match(
+        &self,
+        device_id: DeviceId,
+        pointer_properties: &[RustPointerProperties],
+    ) -> bool {
+        let Some(pointers) = self.touching_pointer_ids_by_device.get(&device_id) else {
+            return false;
+        };
+
+        for pointer_property in pointer_properties.iter() {
+            let pointer_id = pointer_property.id;
+            if !pointers.contains(&pointer_id) {
+                return false;
+            }
+        }
+        true
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::DeviceId;
+    use crate::Flags;
+    use crate::InputVerifier;
+    use crate::RustPointerProperties;
+    #[test]
+    fn single_pointer_stream() {
+        let mut verifier = InputVerifier::new("Test");
+        let pointer_properties = Vec::from([RustPointerProperties { id: 0 }]);
+        assert!(verifier
+            .process_movement(
+                DeviceId(1),
+                input_bindgen::AMOTION_EVENT_ACTION_DOWN,
+                &pointer_properties,
+                Flags::empty(),
+            )
+            .is_ok());
+        assert!(verifier
+            .process_movement(
+                DeviceId(1),
+                input_bindgen::AMOTION_EVENT_ACTION_MOVE,
+                &pointer_properties,
+                Flags::empty(),
+            )
+            .is_ok());
+        assert!(verifier
+            .process_movement(
+                DeviceId(1),
+                input_bindgen::AMOTION_EVENT_ACTION_UP,
+                &pointer_properties,
+                Flags::empty(),
+            )
+            .is_ok());
+    }
+
+    #[test]
+    fn multi_device_stream() {
+        let mut verifier = InputVerifier::new("Test");
+        let pointer_properties = Vec::from([RustPointerProperties { id: 0 }]);
+        assert!(verifier
+            .process_movement(
+                DeviceId(1),
+                input_bindgen::AMOTION_EVENT_ACTION_DOWN,
+                &pointer_properties,
+                Flags::empty(),
+            )
+            .is_ok());
+        assert!(verifier
+            .process_movement(
+                DeviceId(1),
+                input_bindgen::AMOTION_EVENT_ACTION_MOVE,
+                &pointer_properties,
+                Flags::empty(),
+            )
+            .is_ok());
+        assert!(verifier
+            .process_movement(
+                DeviceId(2),
+                input_bindgen::AMOTION_EVENT_ACTION_DOWN,
+                &pointer_properties,
+                Flags::empty(),
+            )
+            .is_ok());
+        assert!(verifier
+            .process_movement(
+                DeviceId(2),
+                input_bindgen::AMOTION_EVENT_ACTION_MOVE,
+                &pointer_properties,
+                Flags::empty(),
+            )
+            .is_ok());
+        assert!(verifier
+            .process_movement(
+                DeviceId(1),
+                input_bindgen::AMOTION_EVENT_ACTION_UP,
+                &pointer_properties,
+                Flags::empty(),
+            )
+            .is_ok());
+    }
+
+    #[test]
+    fn test_invalid_up() {
+        let mut verifier = InputVerifier::new("Test");
+        let pointer_properties = Vec::from([RustPointerProperties { id: 0 }]);
+        assert!(verifier
+            .process_movement(
+                DeviceId(1),
+                input_bindgen::AMOTION_EVENT_ACTION_UP,
+                &pointer_properties,
+                Flags::empty(),
+            )
+            .is_err());
+    }
+}
diff --git a/libs/nativewindow/include/system/window.h b/libs/nativewindow/include/system/window.h
index 0fee3c1..edaa422 100644
--- a/libs/nativewindow/include/system/window.h
+++ b/libs/nativewindow/include/system/window.h
@@ -1066,12 +1066,33 @@
                            (int)compatibility, (int)changeFrameRateStrategy);
 }
 
+struct ANativeWindowFrameTimelineInfo {
+    // Frame Id received from ANativeWindow_getNextFrameId.
+    uint64_t frameNumber;
+
+    // VsyncId received from the Choreographer callback that started this frame.
+    int64_t frameTimelineVsyncId;
+
+    // Input Event ID received from the input event that started this frame.
+    int32_t inputEventId;
+
+    // The time which this frame rendering started (i.e. when Choreographer callback actually run)
+    int64_t startTimeNanos;
+
+    // Whether or not to use the vsyncId to determine the refresh rate. Used for TextureView only.
+    int32_t useForRefreshRateSelection;
+
+    // The VsyncId of a frame that was not drawn and squashed into this frame.
+    // Used for UI thread updates that were not picked up by RenderThread on time.
+    int64_t skippedFrameVsyncId;
+
+    // The start time of a frame that was not drawn and squashed into this frame.
+    int64_t skippedFrameStartTimeNanos;
+};
+
 static inline int native_window_set_frame_timeline_info(
-        struct ANativeWindow* window, uint64_t frameNumber, int64_t frameTimelineVsyncId,
-        int32_t inputEventId, int64_t startTimeNanos, int32_t useForRefreshRateSelection) {
-    return window->perform(window, NATIVE_WINDOW_SET_FRAME_TIMELINE_INFO, frameNumber,
-                           frameTimelineVsyncId, inputEventId, startTimeNanos,
-                           useForRefreshRateSelection);
+        struct ANativeWindow* window, struct ANativeWindowFrameTimelineInfo frameTimelineInfo) {
+    return window->perform(window, NATIVE_WINDOW_SET_FRAME_TIMELINE_INFO, frameTimelineInfo);
 }
 
 // ------------------------------------------------------------------------------------------------
diff --git a/libs/ultrahdr/fuzzer/ultrahdr_enc_fuzzer.cpp b/libs/ultrahdr/fuzzer/ultrahdr_enc_fuzzer.cpp
index acb9b79..bbe58e0 100644
--- a/libs/ultrahdr/fuzzer/ultrahdr_enc_fuzzer.cpp
+++ b/libs/ultrahdr/fuzzer/ultrahdr_enc_fuzzer.cpp
@@ -249,7 +249,7 @@
                         jpegGainMap.data = gainMapEncoder.getCompressedImagePtr();
                         jpegGainMap.colorGamut = ULTRAHDR_COLORGAMUT_UNSPECIFIED;
                         ultrahdr_metadata_struct metadata;
-                        metadata.version = "1.3.1";
+                        metadata.version = "1.0";
                         if (tf == ULTRAHDR_TF_HLG) {
                             metadata.maxContentBoost = kHlgMaxNits / kSdrWhiteNits;
                         } else if (tf == ULTRAHDR_TF_PQ) {
@@ -258,6 +258,11 @@
                             metadata.maxContentBoost = 1.0f;
                         }
                         metadata.minContentBoost = 1.0f;
+                        metadata.gamma = 1.0f;
+                        metadata.offsetSdr = 0.0f;
+                        metadata.offsetHdr = 0.0f;
+                        metadata.hdrCapacityMin = 1.0f;
+                        metadata.hdrCapacityMax = metadata.maxContentBoost;
                         status = jpegHdr.encodeJPEGR(&jpegImg, &jpegGainMap, &metadata, &jpegImgR);
                     }
                 }
diff --git a/libs/ultrahdr/include/ultrahdr/gainmapmath.h b/libs/ultrahdr/include/ultrahdr/gainmapmath.h
index 13832db..edf152d 100644
--- a/libs/ultrahdr/include/ultrahdr/gainmapmath.h
+++ b/libs/ultrahdr/include/ultrahdr/gainmapmath.h
@@ -414,6 +414,10 @@
 /*
  * Calculate the 8-bit unsigned integer gain value for the given SDR and HDR
  * luminances in linear space, and the hdr ratio to encode against.
+ *
+ * Note: since this library always uses gamma of 1.0, offsetSdr of 0.0, and
+ * offsetHdr of 0.0, this function doesn't handle different metadata values for
+ * these fields.
  */
 uint8_t encodeGain(float y_sdr, float y_hdr, ultrahdr_metadata_ptr metadata);
 uint8_t encodeGain(float y_sdr, float y_hdr, ultrahdr_metadata_ptr metadata,
@@ -422,6 +426,10 @@
 /*
  * Calculates the linear luminance in nits after applying the given gain
  * value, with the given hdr ratio, to the given sdr input in the range [0, 1].
+ *
+ * Note: similar to encodeGain(), this function only supports gamma 1.0,
+ * offsetSdr 0.0, offsetHdr 0.0, hdrCapacityMin 1.0, and hdrCapacityMax equal to
+ * gainMapMax, as this library encodes.
  */
 Color applyGain(Color e, float gain, ultrahdr_metadata_ptr metadata);
 Color applyGain(Color e, float gain, ultrahdr_metadata_ptr metadata, float displayBoost);
diff --git a/libs/ultrahdr/include/ultrahdr/jpegr.h b/libs/ultrahdr/include/ultrahdr/jpegr.h
index 9546ca4..a35fd30 100644
--- a/libs/ultrahdr/include/ultrahdr/jpegr.h
+++ b/libs/ultrahdr/include/ultrahdr/jpegr.h
@@ -222,7 +222,11 @@
      * Decompress JPEGR image.
      *
      * This method assumes that the JPEGR image contains an ICC profile with primaries that match
-     * those of a color gamut that this library is aware of; Bt.709, Display-P3, or Bt.2100.
+     * those of a color gamut that this library is aware of; Bt.709, Display-P3, or Bt.2100. It also
+     * assumes the base image uses the sRGB transfer function.
+     *
+     * This method only supports single gain map metadata values for fields that allow multi-channel
+     * metadata values.
      *
      * @param compressed_jpegr_image compressed JPEGR image.
      * @param dest destination of the uncompressed JPEGR image.
@@ -265,6 +269,9 @@
     /*
     * Gets Info from JPEGR file without decoding it.
     *
+    * This method only supports single gain map metadata values for fields that allow multi-channel
+    * metadata values.
+    *
     * The output is filled jpegr_info structure
     * @param compressed_jpegr_image compressed JPEGR image
     * @param jpegr_info pointer to output JPEGR info. Members of jpegr_info
diff --git a/libs/ultrahdr/include/ultrahdr/jpegrerrorcode.h b/libs/ultrahdr/include/ultrahdr/jpegrerrorcode.h
index 9f59c3e..0641232 100644
--- a/libs/ultrahdr/include/ultrahdr/jpegrerrorcode.h
+++ b/libs/ultrahdr/include/ultrahdr/jpegrerrorcode.h
@@ -42,6 +42,8 @@
     ERROR_JPEGR_BUFFER_TOO_SMALL        = JPEGR_IO_ERROR_BASE - 4,
     ERROR_JPEGR_INVALID_COLORGAMUT      = JPEGR_IO_ERROR_BASE - 5,
     ERROR_JPEGR_INVALID_TRANS_FUNC      = JPEGR_IO_ERROR_BASE - 6,
+    ERROR_JPEGR_INVALID_METADATA        = JPEGR_IO_ERROR_BASE - 7,
+    ERROR_JPEGR_UNSUPPORTED_METADATA    = JPEGR_IO_ERROR_BASE - 8,
 
     JPEGR_RUNTIME_ERROR_BASE            = -20000,
     ERROR_JPEGR_ENCODE_ERROR            = JPEGR_RUNTIME_ERROR_BASE - 1,
diff --git a/libs/ultrahdr/include/ultrahdr/ultrahdr.h b/libs/ultrahdr/include/ultrahdr/ultrahdr.h
index 21751b4..17cc971 100644
--- a/libs/ultrahdr/include/ultrahdr/ultrahdr.h
+++ b/libs/ultrahdr/include/ultrahdr/ultrahdr.h
@@ -49,14 +49,28 @@
 
 /*
  * Holds information for gain map related metadata.
+ *
+ * Not: all values stored in linear. This differs from the metadata encoding in XMP, where
+ * maxContentBoost (aka gainMapMax), minContentBoost (aka gainMapMin), hdrCapacityMin, and
+ * hdrCapacityMax are stored in log2 space.
  */
 struct ultrahdr_metadata_struct {
-  // Ultra HDR library version
+  // Ultra HDR format version
   std::string version;
   // Max Content Boost for the map
   float maxContentBoost;
   // Min Content Boost for the map
   float minContentBoost;
+  // Gamma of the map data
+  float gamma;
+  // Offset for SDR data in map calculations
+  float offsetSdr;
+  // Offset for HDR data in map calculations
+  float offsetHdr;
+  // HDR capacity to apply the map at all
+  float hdrCapacityMin;
+  // HDR capacity to apply the map completely
+  float hdrCapacityMax;
 };
 typedef struct ultrahdr_metadata_struct* ultrahdr_metadata_ptr;
 
diff --git a/libs/ultrahdr/jpegr.cpp b/libs/ultrahdr/jpegr.cpp
index 9af5af7..9c57f34 100644
--- a/libs/ultrahdr/jpegr.cpp
+++ b/libs/ultrahdr/jpegr.cpp
@@ -644,13 +644,18 @@
   ultrahdr_metadata_struct uhdr_metadata;
   if (!getMetadataFromXMP(static_cast<uint8_t*>(gain_map_decoder.getXMPPtr()),
                           gain_map_decoder.getXMPSize(), &uhdr_metadata)) {
-    return ERROR_JPEGR_DECODE_ERROR;
+    return ERROR_JPEGR_INVALID_METADATA;
   }
 
   if (metadata != nullptr) {
       metadata->version = uhdr_metadata.version;
       metadata->minContentBoost = uhdr_metadata.minContentBoost;
       metadata->maxContentBoost = uhdr_metadata.maxContentBoost;
+      metadata->gamma = uhdr_metadata.gamma;
+      metadata->offsetSdr = uhdr_metadata.offsetSdr;
+      metadata->offsetHdr = uhdr_metadata.offsetHdr;
+      metadata->hdrCapacityMin = uhdr_metadata.hdrCapacityMin;
+      metadata->hdrCapacityMax = uhdr_metadata.hdrCapacityMax;
   }
 
   if (output_format == ULTRAHDR_OUTPUT_SDR) {
@@ -840,6 +845,12 @@
 
   metadata->maxContentBoost = hdr_white_nits / kSdrWhiteNits;
   metadata->minContentBoost = 1.0f;
+  metadata->gamma = 1.0f;
+  metadata->offsetSdr = 0.0f;
+  metadata->offsetHdr = 0.0f;
+  metadata->hdrCapacityMin = 1.0f;
+  metadata->hdrCapacityMax = metadata->maxContentBoost;
+
   float log2MinBoost = log2(metadata->minContentBoost);
   float log2MaxBoost = log2(metadata->maxContentBoost);
 
@@ -958,6 +969,26 @@
     return ERROR_JPEGR_INVALID_NULL_PTR;
   }
 
+  if (metadata->version.compare("1.0")) {
+      ALOGE("Unsupported metadata version: %s", metadata->version.c_str());
+      return ERROR_JPEGR_UNSUPPORTED_METADATA;
+  }
+  if (metadata->gamma != 1.0f) {
+      ALOGE("Unsupported metadata gamma: %f", metadata->gamma);
+      return ERROR_JPEGR_UNSUPPORTED_METADATA;
+  }
+  if (metadata->offsetSdr != 0.0f || metadata->offsetHdr != 0.0f) {
+      ALOGE("Unsupported metadata offset sdr, hdr: %f, %f", metadata->offsetSdr,
+            metadata->offsetHdr);
+      return ERROR_JPEGR_UNSUPPORTED_METADATA;
+  }
+  if (metadata->hdrCapacityMin != metadata->minContentBoost
+   || metadata->hdrCapacityMax != metadata->maxContentBoost) {
+      ALOGE("Unsupported metadata hdr capacity min, max: %f, %f", metadata->hdrCapacityMin,
+            metadata->hdrCapacityMax);
+      return ERROR_JPEGR_UNSUPPORTED_METADATA;
+  }
+
   // TODO: remove once map scaling factor is computed based on actual map dims
   size_t image_width = uncompressed_yuv_420_image->width;
   size_t image_height = uncompressed_yuv_420_image->height;
@@ -1180,12 +1211,33 @@
     return ERROR_JPEGR_INVALID_NULL_PTR;
   }
 
-  if (metadata->minContentBoost < 1.0f || metadata->maxContentBoost < metadata->minContentBoost) {
+  if (metadata->version.compare("1.0")) {
+    ALOGE("received bad value for version: %s", metadata->version.c_str());
+    return ERROR_JPEGR_INVALID_INPUT_TYPE;
+  }
+  if (metadata->maxContentBoost < metadata->minContentBoost) {
     ALOGE("received bad value for content boost min %f, max %f", metadata->minContentBoost,
            metadata->maxContentBoost);
     return ERROR_JPEGR_INVALID_INPUT_TYPE;
   }
 
+  if (metadata->hdrCapacityMax < metadata->hdrCapacityMin || metadata->hdrCapacityMin < 1.0f) {
+    ALOGE("received bad value for hdr capacity min %f, max %f", metadata->hdrCapacityMin,
+           metadata->hdrCapacityMax);
+    return ERROR_JPEGR_INVALID_INPUT_TYPE;
+  }
+
+  if (metadata->offsetSdr < 0.0f || metadata->offsetHdr < 0.0f) {
+    ALOGE("received bad value for offset sdr %f, hdr %f", metadata->offsetSdr,
+           metadata->offsetHdr);
+    return ERROR_JPEGR_INVALID_INPUT_TYPE;
+  }
+
+  if (metadata->gamma <= 0.0f) {
+    ALOGE("received bad value for gamma %f", metadata->gamma);
+    return ERROR_JPEGR_INVALID_INPUT_TYPE;
+  }
+
   const string nameSpace = "http://ns.adobe.com/xap/1.0/";
   const int nameSpaceLength = nameSpace.size() + 1;  // need to count the null terminator
 
diff --git a/libs/ultrahdr/jpegrutils.cpp b/libs/ultrahdr/jpegrutils.cpp
index 6430af1..c434eb6 100644
--- a/libs/ultrahdr/jpegrutils.cpp
+++ b/libs/ultrahdr/jpegrutils.cpp
@@ -113,6 +113,15 @@
 
     XMPXmlHandler() : XmlHandler() {
         state = NotStrarted;
+        versionFound = false;
+        minContentBoostFound = false;
+        maxContentBoostFound = false;
+        gammaFound = false;
+        offsetSdrFound = false;
+        offsetHdrFound = false;
+        hdrCapacityMinFound = false;
+        hdrCapacityMaxFound = false;
+        baseRenditionIsHdrFound = false;
     }
 
     enum ParseState {
@@ -147,10 +156,24 @@
         string val;
         if (state == Started) {
             if (context.BuildTokenValue(&val)) {
-                if (!val.compare(maxContentBoostAttrName)) {
+                if (!val.compare(versionAttrName)) {
+                    lastAttributeName = versionAttrName;
+                } else if (!val.compare(maxContentBoostAttrName)) {
                     lastAttributeName = maxContentBoostAttrName;
                 } else if (!val.compare(minContentBoostAttrName)) {
                     lastAttributeName = minContentBoostAttrName;
+                } else if (!val.compare(gammaAttrName)) {
+                    lastAttributeName = gammaAttrName;
+                } else if (!val.compare(offsetSdrAttrName)) {
+                    lastAttributeName = offsetSdrAttrName;
+                } else if (!val.compare(offsetHdrAttrName)) {
+                    lastAttributeName = offsetHdrAttrName;
+                } else if (!val.compare(hdrCapacityMinAttrName)) {
+                    lastAttributeName = hdrCapacityMinAttrName;
+                } else if (!val.compare(hdrCapacityMaxAttrName)) {
+                    lastAttributeName = hdrCapacityMaxAttrName;
+                } else if (!val.compare(baseRenditionIsHdrAttrName)) {
+                    lastAttributeName = baseRenditionIsHdrAttrName;
                 } else {
                     lastAttributeName = "";
                 }
@@ -163,18 +186,52 @@
         string val;
         if (state == Started) {
             if (context.BuildTokenValue(&val, true)) {
-                if (!lastAttributeName.compare(maxContentBoostAttrName)) {
+                if (!lastAttributeName.compare(versionAttrName)) {
+                    versionStr = val;
+                    versionFound = true;
+                } else if (!lastAttributeName.compare(maxContentBoostAttrName)) {
                     maxContentBoostStr = val;
+                    maxContentBoostFound = true;
                 } else if (!lastAttributeName.compare(minContentBoostAttrName)) {
                     minContentBoostStr = val;
+                    minContentBoostFound = true;
+                } else if (!lastAttributeName.compare(gammaAttrName)) {
+                    gammaStr = val;
+                    gammaFound = true;
+                } else if (!lastAttributeName.compare(offsetSdrAttrName)) {
+                    offsetSdrStr = val;
+                    offsetSdrFound = true;
+                } else if (!lastAttributeName.compare(offsetHdrAttrName)) {
+                    offsetHdrStr = val;
+                    offsetHdrFound = true;
+                } else if (!lastAttributeName.compare(hdrCapacityMinAttrName)) {
+                    hdrCapacityMinStr = val;
+                    hdrCapacityMinFound = true;
+                } else if (!lastAttributeName.compare(hdrCapacityMaxAttrName)) {
+                    hdrCapacityMaxStr = val;
+                    hdrCapacityMaxFound = true;
+                } else if (!lastAttributeName.compare(baseRenditionIsHdrAttrName)) {
+                    baseRenditionIsHdrStr = val;
+                    baseRenditionIsHdrFound = true;
                 }
             }
         }
         return context.GetResult();
     }
 
-    bool getMaxContentBoost(float* max_content_boost) {
+    bool getVersion(string* version, bool* present) {
         if (state == Done) {
+            *version = versionStr;
+            *present = versionFound;
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    bool getMaxContentBoost(float* max_content_boost, bool* present) {
+        if (state == Done) {
+            *present = maxContentBoostFound;
             stringstream ss(maxContentBoostStr);
             float val;
             if (ss >> val) {
@@ -188,8 +245,9 @@
         }
     }
 
-    bool getMinContentBoost(float* min_content_boost) {
+    bool getMinContentBoost(float* min_content_boost, bool* present) {
         if (state == Done) {
+            *present = minContentBoostFound;
             stringstream ss(minContentBoostStr);
             float val;
             if (ss >> val) {
@@ -203,12 +261,141 @@
         }
     }
 
+    bool getGamma(float* gamma, bool* present) {
+        if (state == Done) {
+            *present = gammaFound;
+            stringstream ss(gammaStr);
+            float val;
+            if (ss >> val) {
+                *gamma = val;
+                return true;
+            } else {
+                return false;
+            }
+        } else {
+            return false;
+        }
+    }
+
+
+    bool getOffsetSdr(float* offset_sdr, bool* present) {
+        if (state == Done) {
+            *present = offsetSdrFound;
+            stringstream ss(offsetSdrStr);
+            float val;
+            if (ss >> val) {
+                *offset_sdr = val;
+                return true;
+            } else {
+                return false;
+            }
+        } else {
+            return false;
+        }
+    }
+
+
+    bool getOffsetHdr(float* offset_hdr, bool* present) {
+        if (state == Done) {
+            *present = offsetHdrFound;
+            stringstream ss(offsetHdrStr);
+            float val;
+            if (ss >> val) {
+                *offset_hdr = val;
+                return true;
+            } else {
+                return false;
+            }
+        } else {
+            return false;
+        }
+    }
+
+
+    bool getHdrCapacityMin(float* hdr_capacity_min, bool* present) {
+        if (state == Done) {
+            *present = hdrCapacityMinFound;
+            stringstream ss(hdrCapacityMinStr);
+            float val;
+            if (ss >> val) {
+                *hdr_capacity_min = exp2(val);
+                return true;
+            } else {
+                return false;
+            }
+        } else {
+            return false;
+        }
+    }
+
+
+    bool getHdrCapacityMax(float* hdr_capacity_max, bool* present) {
+        if (state == Done) {
+            *present = hdrCapacityMaxFound;
+            stringstream ss(hdrCapacityMaxStr);
+            float val;
+            if (ss >> val) {
+                *hdr_capacity_max = exp2(val);
+                return true;
+            } else {
+                return false;
+            }
+        } else {
+            return false;
+        }
+    }
+
+
+    bool getBaseRenditionIsHdr(bool* base_rendition_is_hdr, bool* present) {
+        if (state == Done) {
+            *present = baseRenditionIsHdrFound;
+            if (!baseRenditionIsHdrStr.compare("False")) {
+                *base_rendition_is_hdr = false;
+                return true;
+            } else if (!baseRenditionIsHdrStr.compare("True")) {
+                *base_rendition_is_hdr = true;
+                return true;
+            } else {
+                return false;
+            }
+        } else {
+            return false;
+        }
+    }
+
+
+
 private:
     static const string containerName;
+
+    static const string versionAttrName;
+    string              versionStr;
+    bool                versionFound;
     static const string maxContentBoostAttrName;
     string              maxContentBoostStr;
+    bool                maxContentBoostFound;
     static const string minContentBoostAttrName;
     string              minContentBoostStr;
+    bool                minContentBoostFound;
+    static const string gammaAttrName;
+    string              gammaStr;
+    bool                gammaFound;
+    static const string offsetSdrAttrName;
+    string              offsetSdrStr;
+    bool                offsetSdrFound;
+    static const string offsetHdrAttrName;
+    string              offsetHdrStr;
+    bool                offsetHdrFound;
+    static const string hdrCapacityMinAttrName;
+    string              hdrCapacityMinStr;
+    bool                hdrCapacityMinFound;
+    static const string hdrCapacityMaxAttrName;
+    string              hdrCapacityMaxStr;
+    bool                hdrCapacityMaxFound;
+    static const string baseRenditionIsHdrAttrName;
+    string              baseRenditionIsHdrStr;
+    bool                baseRenditionIsHdrFound;
+
     string              lastAttributeName;
     ParseState          state;
 };
@@ -253,8 +440,15 @@
 const string kMapBaseRenditionIsHDR = Name(kGainMapPrefix, "BaseRenditionIsHDR");
 
 // GainMap XMP constants - names for XMP handlers
+const string XMPXmlHandler::versionAttrName = kMapVersion;
 const string XMPXmlHandler::minContentBoostAttrName = kMapGainMapMin;
 const string XMPXmlHandler::maxContentBoostAttrName = kMapGainMapMax;
+const string XMPXmlHandler::gammaAttrName = kMapGamma;
+const string XMPXmlHandler::offsetSdrAttrName = kMapOffsetSdr;
+const string XMPXmlHandler::offsetHdrAttrName = kMapOffsetHdr;
+const string XMPXmlHandler::hdrCapacityMinAttrName = kMapHDRCapacityMin;
+const string XMPXmlHandler::hdrCapacityMaxAttrName = kMapHDRCapacityMax;
+const string XMPXmlHandler::baseRenditionIsHdrAttrName = kMapBaseRenditionIsHDR;
 
 bool getMetadataFromXMP(uint8_t* xmp_data, size_t xmp_size, ultrahdr_metadata_struct* metadata) {
     string nameSpace = "http://ns.adobe.com/xap/1.0/\0";
@@ -291,11 +485,48 @@
         return false;
     }
 
-    if (!handler.getMaxContentBoost(&metadata->maxContentBoost)) {
+    // Apply default values to any not-present fields, except for Version,
+    // maxContentBoost, and hdrCapacityMax, which are required. Return false if
+    // we encounter a present field that couldn't be parsed, since this
+    // indicates it is invalid (eg. string where there should be a float).
+    bool present = false;
+    if (!handler.getVersion(&metadata->version, &present) || !present) {
         return false;
     }
+    if (!handler.getMaxContentBoost(&metadata->maxContentBoost, &present) || !present) {
+        return false;
+    }
+    if (!handler.getHdrCapacityMax(&metadata->hdrCapacityMax, &present) || !present) {
+        return false;
+    }
+    if (!handler.getMinContentBoost(&metadata->minContentBoost, &present)) {
+        if (present) return false;
+        metadata->minContentBoost = 1.0f;
+    }
+    if (!handler.getGamma(&metadata->gamma, &present)) {
+        if (present) return false;
+        metadata->gamma = 1.0f;
+    }
+    if (!handler.getOffsetSdr(&metadata->offsetSdr, &present)) {
+        if (present) return false;
+        metadata->offsetSdr = 1.0f / 64.0f;
+    }
+    if (!handler.getOffsetHdr(&metadata->offsetHdr, &present)) {
+        if (present) return false;
+        metadata->offsetHdr = 1.0f / 64.0f;
+    }
+    if (!handler.getHdrCapacityMin(&metadata->hdrCapacityMin, &present)) {
+        if (present) return false;
+        metadata->hdrCapacityMin = 1.0f;
+    }
 
-    if (!handler.getMinContentBoost(&metadata->minContentBoost)) {
+    bool base_rendition_is_hdr;
+    if (!handler.getBaseRenditionIsHdr(&base_rendition_is_hdr, &present)) {
+        if (present) return false;
+        base_rendition_is_hdr = false;
+    }
+    if (base_rendition_is_hdr) {
+        ALOGE("Base rendition of HDR is not supported!");
         return false;
     }
 
@@ -355,12 +586,11 @@
   writer.WriteAttributeNameAndValue(kMapVersion, metadata.version);
   writer.WriteAttributeNameAndValue(kMapGainMapMin, log2(metadata.minContentBoost));
   writer.WriteAttributeNameAndValue(kMapGainMapMax, log2(metadata.maxContentBoost));
-  writer.WriteAttributeNameAndValue(kMapGamma, "1");
-  writer.WriteAttributeNameAndValue(kMapOffsetSdr, "0");
-  writer.WriteAttributeNameAndValue(kMapOffsetHdr, "0");
-  writer.WriteAttributeNameAndValue(
-      kMapHDRCapacityMin, std::max(log2(metadata.minContentBoost), 0.0f));
-  writer.WriteAttributeNameAndValue(kMapHDRCapacityMax, log2(metadata.maxContentBoost));
+  writer.WriteAttributeNameAndValue(kMapGamma, metadata.gamma);
+  writer.WriteAttributeNameAndValue(kMapOffsetSdr, metadata.offsetSdr);
+  writer.WriteAttributeNameAndValue(kMapOffsetHdr, metadata.offsetHdr);
+  writer.WriteAttributeNameAndValue(kMapHDRCapacityMin, log2(metadata.hdrCapacityMin));
+  writer.WriteAttributeNameAndValue(kMapHDRCapacityMax, log2(metadata.hdrCapacityMax));
   writer.WriteAttributeNameAndValue(kMapBaseRenditionIsHDR, "False");
   writer.FinishWriting();
 
diff --git a/libs/ultrahdr/tests/data/minnie-320x240-yuv-icc.jpg b/libs/ultrahdr/tests/data/minnie-320x240-yuv-icc.jpg
index f61e0e8..c7f4538 100644
--- a/libs/ultrahdr/tests/data/minnie-320x240-yuv-icc.jpg
+++ b/libs/ultrahdr/tests/data/minnie-320x240-yuv-icc.jpg
Binary files differ
diff --git a/libs/ultrahdr/tests/jpegr_test.cpp b/libs/ultrahdr/tests/jpegr_test.cpp
index d482ea1..41d55ec 100644
--- a/libs/ultrahdr/tests/jpegr_test.cpp
+++ b/libs/ultrahdr/tests/jpegr_test.cpp
@@ -819,6 +819,52 @@
   EXPECT_NE(OK, jpegRCodec.encodeJPEGR(
       &jpegR, nullptr, nullptr, &jpegR)) << "fail, API allows nullptr gainmap image";
 
+  // test metadata
+  ultrahdr_metadata_struct good_metadata;
+  good_metadata.version = "1.0";
+  good_metadata.minContentBoost = 1.0f;
+  good_metadata.maxContentBoost = 2.0f;
+  good_metadata.gamma = 1.0f;
+  good_metadata.offsetSdr = 0.0f;
+  good_metadata.offsetHdr = 0.0f;
+  good_metadata.hdrCapacityMin = 1.0f;
+  good_metadata.hdrCapacityMax = 2.0f;
+
+  ultrahdr_metadata_struct metadata = good_metadata;
+  metadata.version = "1.1";
+  EXPECT_NE(OK, jpegRCodec.encodeJPEGR(
+      &jpegR, nullptr, &metadata, &jpegR)) << "fail, API allows bad metadata version";
+
+  metadata = good_metadata;
+  metadata.minContentBoost = 3.0f;
+  EXPECT_NE(OK, jpegRCodec.encodeJPEGR(
+      &jpegR, nullptr, &metadata, &jpegR)) << "fail, API allows bad metadata content boost";
+
+  metadata = good_metadata;
+  metadata.gamma = -0.1f;
+  EXPECT_NE(OK, jpegRCodec.encodeJPEGR(
+      &jpegR, nullptr, &metadata, &jpegR)) << "fail, API allows bad metadata gamma";
+
+  metadata = good_metadata;
+  metadata.offsetSdr = -0.1f;
+  EXPECT_NE(OK, jpegRCodec.encodeJPEGR(
+      &jpegR, nullptr, &metadata, &jpegR)) << "fail, API allows bad metadata offset sdr";
+
+  metadata = good_metadata;
+  metadata.offsetHdr = -0.1f;
+  EXPECT_NE(OK, jpegRCodec.encodeJPEGR(
+      &jpegR, nullptr, &metadata, &jpegR)) << "fail, API allows bad metadata offset hdr";
+
+  metadata = good_metadata;
+  metadata.hdrCapacityMax = 0.5f;
+  EXPECT_NE(OK, jpegRCodec.encodeJPEGR(
+      &jpegR, nullptr, &metadata, &jpegR)) << "fail, API allows bad metadata hdr capacity max";
+
+  metadata = good_metadata;
+  metadata.hdrCapacityMin = 0.5f;
+  EXPECT_NE(OK, jpegRCodec.encodeJPEGR(
+      &jpegR, nullptr, &metadata, &jpegR)) << "fail, API allows bad metadata hdr capacity min";
+
   free(jpegR.data);
 }
 
@@ -864,8 +910,13 @@
 TEST_F(JpegRTest, writeXmpThenRead) {
   ultrahdr_metadata_struct metadata_expected;
   metadata_expected.version = "1.0";
-  metadata_expected.maxContentBoost = 1.25;
-  metadata_expected.minContentBoost = 0.75;
+  metadata_expected.maxContentBoost = 1.25f;
+  metadata_expected.minContentBoost = 0.75f;
+  metadata_expected.gamma = 1.0f;
+  metadata_expected.offsetSdr = 0.0f;
+  metadata_expected.offsetHdr = 0.0f;
+  metadata_expected.hdrCapacityMin = 1.0f;
+  metadata_expected.hdrCapacityMax = metadata_expected.maxContentBoost;
   const std::string nameSpace = "http://ns.adobe.com/xap/1.0/\0";
   const int nameSpaceLength = nameSpace.size() + 1;  // need to count the null terminator
 
@@ -882,6 +933,11 @@
   EXPECT_TRUE(getMetadataFromXMP(xmpData.data(), xmpData.size(), &metadata_read));
   EXPECT_FLOAT_EQ(metadata_expected.maxContentBoost, metadata_read.maxContentBoost);
   EXPECT_FLOAT_EQ(metadata_expected.minContentBoost, metadata_read.minContentBoost);
+  EXPECT_FLOAT_EQ(metadata_expected.gamma, metadata_read.gamma);
+  EXPECT_FLOAT_EQ(metadata_expected.offsetSdr, metadata_read.offsetSdr);
+  EXPECT_FLOAT_EQ(metadata_expected.offsetHdr, metadata_read.offsetHdr);
+  EXPECT_FLOAT_EQ(metadata_expected.hdrCapacityMin, metadata_read.hdrCapacityMin);
+  EXPECT_FLOAT_EQ(metadata_expected.hdrCapacityMax, metadata_read.hdrCapacityMax);
 }
 
 /* Test Encode API-0 */
@@ -1297,9 +1353,7 @@
 
   JpegRBenchmark benchmark;
 
-  ultrahdr_metadata_struct metadata = { .version = "1.0",
-                              .maxContentBoost = 8.0f,
-                              .minContentBoost = 1.0f / 8.0f };
+  ultrahdr_metadata_struct metadata = { .version = "1.0" };
 
   jpegr_uncompressed_struct map = { .data = NULL,
                                     .width = 0,
diff --git a/opengl/libs/EGL/Loader.cpp b/opengl/libs/EGL/Loader.cpp
index 2c3ce16..bb3b43a 100644
--- a/opengl/libs/EGL/Loader.cpp
+++ b/opengl/libs/EGL/Loader.cpp
@@ -137,12 +137,14 @@
 #endif
 #endif
 
-static const char* DRIVER_SUFFIX_PROPERTY = "ro.hardware.egl";
+static const char* PERSIST_DRIVER_SUFFIX_PROPERTY = "persist.graphics.egl";
+static const char* RO_DRIVER_SUFFIX_PROPERTY = "ro.hardware.egl";
+static const char* RO_BOARD_PLATFORM_PROPERTY = "ro.board.platform";
 
 static const char* HAL_SUBNAME_KEY_PROPERTIES[3] = {
-        "persist.graphics.egl",
-        DRIVER_SUFFIX_PROPERTY,
-        "ro.board.platform",
+        PERSIST_DRIVER_SUFFIX_PROPERTY,
+        RO_DRIVER_SUFFIX_PROPERTY,
+        RO_BOARD_PLATFORM_PROPERTY,
 };
 
 static bool should_unload_system_driver(egl_connection_t* cnx) {
@@ -245,17 +247,20 @@
                 continue;
             }
             hnd = attempt_to_load_system_driver(cnx, prop.c_str(), true);
-            if (hnd) {
-                break;
-            } else if (strcmp(key, DRIVER_SUFFIX_PROPERTY) == 0) {
+            if (!hnd) {
+                ALOGD("Failed to load drivers from property %s with value %s", key, prop.c_str());
                 failToLoadFromDriverSuffixProperty = true;
             }
+
+            // Abort regardless of whether subsequent properties are set, the value must be set
+            // correctly with the first property that has a value.
+            break;
         }
     }
 
     if (!hnd) {
-        // Can't find graphics driver by appending system properties, now search for the exact name
-        // without any suffix of the GLES userspace driver in both locations.
+        // Can't find graphics driver by appending the value from system properties, now search for
+        // the exact name without any suffix of the GLES userspace driver in both locations.
         // i.e.:
         //      libGLES.so, or:
         //      libEGL.so, libGLESv1_CM.so, libGLESv2.so
diff --git a/services/inputflinger/Android.bp b/services/inputflinger/Android.bp
index ffcc967..86b3bde 100644
--- a/services/inputflinger/Android.bp
+++ b/services/inputflinger/Android.bp
@@ -230,6 +230,9 @@
         "inputflinger",
         "libinputflingerhost",
 
+        // rust targets
+        "libinput_rust_test",
+
         // native fuzzers
         "inputflinger_latencytracker_fuzzer",
         "inputflinger_cursor_input_fuzzer",
diff --git a/services/inputflinger/InputDeviceMetricsCollector.cpp b/services/inputflinger/InputDeviceMetricsCollector.cpp
index f92c4f4..55b1df1 100644
--- a/services/inputflinger/InputDeviceMetricsCollector.cpp
+++ b/services/inputflinger/InputDeviceMetricsCollector.cpp
@@ -31,7 +31,7 @@
 
 namespace {
 
-constexpr nanoseconds DEFAULT_USAGE_SESSION_TIMEOUT = std::chrono::seconds(5);
+constexpr nanoseconds DEFAULT_USAGE_SESSION_TIMEOUT = std::chrono::minutes(2);
 
 /**
  * Log debug messages about metrics events logged to statsd.
diff --git a/services/inputflinger/InputManager.cpp b/services/inputflinger/InputManager.cpp
index 1a9f47d..37b3187 100644
--- a/services/inputflinger/InputManager.cpp
+++ b/services/inputflinger/InputManager.cpp
@@ -34,7 +34,7 @@
 namespace android {
 
 static const bool ENABLE_INPUT_DEVICE_USAGE_METRICS =
-        sysprop::InputProperties::enable_input_device_usage_metrics().value_or(false);
+        sysprop::InputProperties::enable_input_device_usage_metrics().value_or(true);
 
 using gui::FocusRequest;
 
diff --git a/services/inputflinger/dispatcher/DebugConfig.cpp b/services/inputflinger/dispatcher/DebugConfig.cpp
index 764194d..12122fd 100644
--- a/services/inputflinger/dispatcher/DebugConfig.cpp
+++ b/services/inputflinger/dispatcher/DebugConfig.cpp
@@ -30,11 +30,10 @@
 bool debugInboundEventDetails() {
     if (!IS_DEBUGGABLE_BUILD) {
         static const bool DEBUG_INBOUND_EVENT_DETAILS =
-                __android_log_is_loggable(ANDROID_LOG_DEBUG, LOG_TAG "InboundEvent",
-                                          ANDROID_LOG_INFO);
+                android::base::ShouldLog(android::base::LogSeverity::DEBUG, LOG_TAG "InboundEvent");
         return DEBUG_INBOUND_EVENT_DETAILS;
     }
-    return __android_log_is_loggable(ANDROID_LOG_DEBUG, LOG_TAG "InboundEvent", ANDROID_LOG_INFO);
+    return android::base::ShouldLog(android::base::LogSeverity::DEBUG, LOG_TAG "InboundEvent");
 }
 
 } // namespace android::inputdispatcher
diff --git a/services/inputflinger/dispatcher/DebugConfig.h b/services/inputflinger/dispatcher/DebugConfig.h
index 0e260a7..7a41d68 100644
--- a/services/inputflinger/dispatcher/DebugConfig.h
+++ b/services/inputflinger/dispatcher/DebugConfig.h
@@ -18,8 +18,7 @@
 
 #define LOG_TAG "InputDispatcher"
 
-#include <log/log.h>
-#include <log/log_event_list.h>
+#include <android-base/logging.h>
 
 namespace android::inputdispatcher {
 
@@ -42,14 +41,14 @@
  * Enable this via "adb shell setprop log.tag.InputDispatcherOutboundEvent DEBUG" (requires restart)
  */
 const bool DEBUG_OUTBOUND_EVENT_DETAILS =
-        __android_log_is_loggable(ANDROID_LOG_DEBUG, LOG_TAG "OutboundEvent", ANDROID_LOG_INFO);
+        android::base::ShouldLog(android::base::LogSeverity::DEBUG, LOG_TAG "OutboundEvent");
 
 /**
  * Log debug messages about the dispatch cycle.
  * Enable this via "adb shell setprop log.tag.InputDispatcherDispatchCycle DEBUG" (requires restart)
  */
 const bool DEBUG_DISPATCH_CYCLE =
-        __android_log_is_loggable(ANDROID_LOG_DEBUG, LOG_TAG "DispatchCycle", ANDROID_LOG_INFO);
+        android::base::ShouldLog(android::base::LogSeverity::DEBUG, LOG_TAG "DispatchCycle");
 
 /**
  * Log debug messages about channel creation
@@ -57,28 +56,28 @@
  * restart)
  */
 const bool DEBUG_CHANNEL_CREATION =
-        __android_log_is_loggable(ANDROID_LOG_DEBUG, LOG_TAG "ChannelCreation", ANDROID_LOG_INFO);
+        android::base::ShouldLog(android::base::LogSeverity::DEBUG, LOG_TAG "ChannelCreation");
 
 /**
  * Log debug messages about input event injection.
  * Enable this via "adb shell setprop log.tag.InputDispatcherInjection DEBUG" (requires restart)
  */
 const bool DEBUG_INJECTION =
-        __android_log_is_loggable(ANDROID_LOG_DEBUG, LOG_TAG "Injection", ANDROID_LOG_INFO);
+        android::base::ShouldLog(android::base::LogSeverity::DEBUG, LOG_TAG "Injection");
 
 /**
  * Log debug messages about input focus tracking.
  * Enable this via "adb shell setprop log.tag.InputDispatcherFocus DEBUG" (requires restart)
  */
 const bool DEBUG_FOCUS =
-        __android_log_is_loggable(ANDROID_LOG_DEBUG, LOG_TAG "Focus", ANDROID_LOG_INFO);
+        android::base::ShouldLog(android::base::LogSeverity::DEBUG, LOG_TAG "Focus");
 
 /**
  * Log debug messages about touch mode event
  * Enable this via "adb shell setprop log.tag.InputDispatcherTouchMode DEBUG" (requires restart)
  */
 const bool DEBUG_TOUCH_MODE =
-        __android_log_is_loggable(ANDROID_LOG_DEBUG, LOG_TAG "TouchMode", ANDROID_LOG_INFO);
+        android::base::ShouldLog(android::base::LogSeverity::DEBUG, LOG_TAG "TouchMode");
 
 /**
  * Log debug messages about touch occlusion
@@ -90,13 +89,20 @@
  * Enable this via "adb shell setprop log.tag.InputDispatcherAppSwitch DEBUG" (requires restart)
  */
 const bool DEBUG_APP_SWITCH =
-        __android_log_is_loggable(ANDROID_LOG_DEBUG, LOG_TAG "AppSwitch", ANDROID_LOG_INFO);
+        android::base::ShouldLog(android::base::LogSeverity::DEBUG, LOG_TAG "AppSwitch");
 
 /**
  * Log debug messages about hover events.
  * Enable this via "adb shell setprop log.tag.InputDispatcherHover DEBUG" (requires restart)
  */
 const bool DEBUG_HOVER =
-        __android_log_is_loggable(ANDROID_LOG_DEBUG, LOG_TAG "Hover", ANDROID_LOG_INFO);
+        android::base::ShouldLog(android::base::LogSeverity::DEBUG, LOG_TAG "Hover");
+
+/**
+ * Crash if a bad stream from InputListener is detected.
+ * Enable this via "adb shell setprop log.tag.InputDispatcherVerifyEvents DEBUG" (requires restart)
+ */
+const bool DEBUG_VERIFY_EVENTS =
+        android::base::ShouldLog(android::base::LogSeverity::DEBUG, LOG_TAG "VerifyEvents");
 
 } // namespace android::inputdispatcher
diff --git a/services/inputflinger/dispatcher/InputDispatcher.cpp b/services/inputflinger/dispatcher/InputDispatcher.cpp
index 055fb6f..b6a9ac5 100644
--- a/services/inputflinger/dispatcher/InputDispatcher.cpp
+++ b/services/inputflinger/dispatcher/InputDispatcher.cpp
@@ -26,6 +26,7 @@
 #include <android/os/IInputConstants.h>
 #include <binder/Binder.h>
 #include <ftl/enum.h>
+#include <log/log_event_list.h>
 #if defined(__ANDROID__)
 #include <gui/SurfaceComposerClient.h>
 #endif
@@ -4348,6 +4349,18 @@
         return;
     }
 
+    if (DEBUG_VERIFY_EVENTS) {
+        auto [it, _] =
+                mVerifiersByDisplay.try_emplace(args.displayId,
+                                                StringPrintf("display %" PRId32, args.displayId));
+        Result<void> result =
+                it->second.processMovement(args.deviceId, args.action, args.pointerCount,
+                                           args.pointerProperties, args.pointerCoords, args.flags);
+        if (!result.ok()) {
+            LOG(FATAL) << "Bad stream: " << result.error() << " caused by " << args.dump();
+        }
+    }
+
     uint32_t policyFlags = args.policyFlags;
     policyFlags |= POLICY_FLAG_TRUSTED;
 
@@ -6692,6 +6705,7 @@
         std::erase(mIneligibleDisplaysForPointerCapture, displayId);
         // Remove the associated touch mode state.
         mTouchModePerDisplay.erase(displayId);
+        mVerifiersByDisplay.erase(displayId);
     } // release lock
 
     // Wake up poll loop since it may need to make new input dispatching choices.
diff --git a/services/inputflinger/dispatcher/InputDispatcher.h b/services/inputflinger/dispatcher/InputDispatcher.h
index 20fe0ca..6635df7 100644
--- a/services/inputflinger/dispatcher/InputDispatcher.h
+++ b/services/inputflinger/dispatcher/InputDispatcher.h
@@ -683,6 +683,7 @@
                                   const std::string& reason) REQUIRES(mLock);
     void updateLastAnrStateLocked(const std::string& windowLabel, const std::string& reason)
             REQUIRES(mLock);
+    std::map<int32_t /*displayId*/, InputVerifier> mVerifiersByDisplay;
     bool afterKeyEventLockedInterruptable(const std::shared_ptr<Connection>& connection,
                                           DispatchEntry* dispatchEntry, KeyEntry& keyEntry,
                                           bool handled) REQUIRES(mLock);
diff --git a/services/inputflinger/tests/EventBuilders.h b/services/inputflinger/tests/EventBuilders.h
index 606a57d..09438e9 100644
--- a/services/inputflinger/tests/EventBuilders.h
+++ b/services/inputflinger/tests/EventBuilders.h
@@ -242,6 +242,10 @@
             mRawYCursorPosition = pointerCoords[0].getY();
         }
 
+        if (mAction == AMOTION_EVENT_ACTION_CANCEL) {
+            addFlag(AMOTION_EVENT_FLAG_CANCELED);
+        }
+
         return {InputEvent::nextId(),
                 mEventTime,
                 /*readTime=*/mEventTime,
diff --git a/services/inputflinger/tests/InputDispatcher_test.cpp b/services/inputflinger/tests/InputDispatcher_test.cpp
index 8b74b25..cef1791 100644
--- a/services/inputflinger/tests/InputDispatcher_test.cpp
+++ b/services/inputflinger/tests/InputDispatcher_test.cpp
@@ -3644,6 +3644,9 @@
                 std::chrono::nanoseconds(interceptKeyTimeout).count());
 }
 
+/**
+ * Keys with ACTION_UP are delivered immediately, even if a long 'intercept key timeout' is set.
+ */
 TEST_F(InputDispatcherTest, InterceptKeyIfKeyUp) {
     std::shared_ptr<FakeApplicationHandle> application = std::make_shared<FakeApplicationHandle>();
     sp<FakeWindowHandle> window = sp<FakeWindowHandle>::make(application, mDispatcher,
@@ -3655,12 +3658,14 @@
 
     window->consumeFocusEvent(true);
 
-    mFakePolicy->setInterceptKeyTimeout(150ms);
     mDispatcher->notifyKey(generateKeyArgs(AKEY_EVENT_ACTION_DOWN, ADISPLAY_ID_DEFAULT));
-    mDispatcher->notifyKey(generateKeyArgs(AKEY_EVENT_ACTION_UP, ADISPLAY_ID_DEFAULT));
-
-    // Window should receive key event immediately when same key up.
     window->consumeKeyDown(ADISPLAY_ID_DEFAULT);
+
+    // Set a value that's significantly larger than the default consumption timeout. If the
+    // implementation is correct, the actual value doesn't matter; it won't slow down the test.
+    mFakePolicy->setInterceptKeyTimeout(600ms);
+    mDispatcher->notifyKey(generateKeyArgs(AKEY_EVENT_ACTION_UP, ADISPLAY_ID_DEFAULT));
+    // Window should receive key event immediately when same key up.
     window->consumeKeyUp(ADISPLAY_ID_DEFAULT);
 }
 
@@ -8724,6 +8729,8 @@
 
     mDispatcher->notifyMotion(generateMotionArgs(AMOTION_EVENT_ACTION_DOWN,
                                                  AINPUT_SOURCE_TOUCHSCREEN, ADISPLAY_ID_DEFAULT));
+    mDispatcher->notifyMotion(generateMotionArgs(AMOTION_EVENT_ACTION_UP, AINPUT_SOURCE_TOUCHSCREEN,
+                                                 ADISPLAY_ID_DEFAULT));
     window->assertNoEvents();
 
     // With the flag cleared, the window should get input
@@ -8765,6 +8772,8 @@
 
     mDispatcher->notifyMotion(generateMotionArgs(AMOTION_EVENT_ACTION_DOWN,
                                                  AINPUT_SOURCE_TOUCHSCREEN, ADISPLAY_ID_DEFAULT));
+    mDispatcher->notifyMotion(generateMotionArgs(AMOTION_EVENT_ACTION_UP, AINPUT_SOURCE_TOUCHSCREEN,
+                                                 ADISPLAY_ID_DEFAULT));
     window->assertNoEvents();
 
     // With the flag cleared, the window should get input
@@ -8806,6 +8815,8 @@
 
     mDispatcher->notifyMotion(generateMotionArgs(AMOTION_EVENT_ACTION_DOWN,
                                                  AINPUT_SOURCE_TOUCHSCREEN, ADISPLAY_ID_DEFAULT));
+    mDispatcher->notifyMotion(generateMotionArgs(AMOTION_EVENT_ACTION_UP, AINPUT_SOURCE_TOUCHSCREEN,
+                                                 ADISPLAY_ID_DEFAULT));
     window->assertNoEvents();
 
     // When the window is no longer obscured because it went on top, it should get input
diff --git a/services/sensorservice/SensorService.cpp b/services/sensorservice/SensorService.cpp
index 398d602..90d7541 100644
--- a/services/sensorservice/SensorService.cpp
+++ b/services/sensorservice/SensorService.cpp
@@ -1055,7 +1055,12 @@
         if (count < 0) {
             if(count == DEAD_OBJECT && device.isReconnecting()) {
                 device.reconnect();
-                continue;
+                // There are no "real" events at this point, but do not skip the rest of the loop
+                // if there are pending runtime events.
+                Mutex::Autolock _l(&mLock);
+                if (mRuntimeSensorEventQueue.empty()) {
+                    continue;
+                }
             } else {
                 ALOGE("sensor poll failed (%s)", strerror(-count));
                 break;
diff --git a/services/surfaceflinger/Layer.cpp b/services/surfaceflinger/Layer.cpp
index cf1b018..f627501 100644
--- a/services/surfaceflinger/Layer.cpp
+++ b/services/surfaceflinger/Layer.cpp
@@ -1349,6 +1349,8 @@
         mDrawingState.bufferSurfaceFrameTX =
                 createSurfaceFrameForBuffer(info, postTime, mTransactionName);
     }
+
+    setFrameTimelineVsyncForSkippedFrames(info, postTime, mTransactionName);
 }
 
 void Layer::setFrameTimelineVsyncForBufferlessTransaction(const FrameTimelineInfo& info,
@@ -1380,11 +1382,13 @@
             it->second = createSurfaceFrameForTransaction(info, postTime);
         }
     }
+
+    setFrameTimelineVsyncForSkippedFrames(info, postTime, mTransactionName);
 }
 
 void Layer::addSurfaceFrameDroppedForBuffer(
-        std::shared_ptr<frametimeline::SurfaceFrame>& surfaceFrame) {
-    surfaceFrame->setDropTime(systemTime());
+        std::shared_ptr<frametimeline::SurfaceFrame>& surfaceFrame, nsecs_t dropTime) {
+    surfaceFrame->setDropTime(dropTime);
     surfaceFrame->setPresentState(PresentState::Dropped);
     mFlinger->mFrameTimeline->addSurfaceFrame(surfaceFrame);
 }
@@ -1434,6 +1438,32 @@
     return surfaceFrame;
 }
 
+void Layer::setFrameTimelineVsyncForSkippedFrames(const FrameTimelineInfo& info, nsecs_t postTime,
+                                                  std::string debugName) {
+    if (info.skippedFrameVsyncId == FrameTimelineInfo::INVALID_VSYNC_ID) {
+        return;
+    }
+
+    FrameTimelineInfo skippedFrameTimelineInfo = info;
+    skippedFrameTimelineInfo.vsyncId = info.skippedFrameVsyncId;
+
+    auto surfaceFrame =
+            mFlinger->mFrameTimeline->createSurfaceFrameForToken(skippedFrameTimelineInfo,
+                                                                 mOwnerPid, mOwnerUid,
+                                                                 getSequence(), mName, debugName,
+                                                                 /*isBuffer*/ false, getGameMode());
+    surfaceFrame->setActualStartTime(skippedFrameTimelineInfo.skippedFrameStartTimeNanos);
+    // For Transactions, the post time is considered to be both queue and acquire fence time.
+    surfaceFrame->setActualQueueTime(postTime);
+    surfaceFrame->setAcquireFenceTime(postTime);
+    const auto fps = mFlinger->mScheduler->getFrameRateOverride(getOwnerUid());
+    if (fps) {
+        surfaceFrame->setRenderRate(*fps);
+    }
+    onSurfaceFrameCreated(surfaceFrame);
+    addSurfaceFrameDroppedForBuffer(surfaceFrame, postTime);
+}
+
 bool Layer::setFrameRateForLayerTreeLegacy(FrameRate frameRate) {
     if (mDrawingState.frameRateForLayerTree == frameRate) {
         return false;
@@ -3067,7 +3097,7 @@
             decrementPendingBufferCount();
             if (mDrawingState.bufferSurfaceFrameTX != nullptr &&
                 mDrawingState.bufferSurfaceFrameTX->getPresentState() != PresentState::Presented) {
-                addSurfaceFrameDroppedForBuffer(mDrawingState.bufferSurfaceFrameTX);
+                addSurfaceFrameDroppedForBuffer(mDrawingState.bufferSurfaceFrameTX, systemTime());
                 mDrawingState.bufferSurfaceFrameTX.reset();
             }
         } else if (EARLY_RELEASE_ENABLED && mLastClientCompositionFence != nullptr) {
diff --git a/services/surfaceflinger/Layer.h b/services/surfaceflinger/Layer.h
index f34fdd9..2fbbbdc 100644
--- a/services/surfaceflinger/Layer.h
+++ b/services/surfaceflinger/Layer.h
@@ -785,8 +785,8 @@
     void setFrameTimelineVsyncForBufferlessTransaction(const FrameTimelineInfo& info,
                                                        nsecs_t postTime);
 
-    void addSurfaceFrameDroppedForBuffer(
-            std::shared_ptr<frametimeline::SurfaceFrame>& surfaceFrame);
+    void addSurfaceFrameDroppedForBuffer(std::shared_ptr<frametimeline::SurfaceFrame>& surfaceFrame,
+                                         nsecs_t dropTime);
     void addSurfaceFramePresentedForBuffer(
             std::shared_ptr<frametimeline::SurfaceFrame>& surfaceFrame, nsecs_t acquireFenceTime,
             nsecs_t currentLatchTime);
@@ -795,6 +795,8 @@
             const FrameTimelineInfo& info, nsecs_t postTime);
     std::shared_ptr<frametimeline::SurfaceFrame> createSurfaceFrameForBuffer(
             const FrameTimelineInfo& info, nsecs_t queueTime, std::string debugName);
+    void setFrameTimelineVsyncForSkippedFrames(const FrameTimelineInfo& info, nsecs_t postTime,
+                                               std::string debugName);
 
     bool setTrustedPresentationInfo(TrustedPresentationThresholds const& thresholds,
                                     TrustedPresentationListener const& listener);
diff --git a/services/surfaceflinger/fuzzer/surfaceflinger_layer_fuzzer.cpp b/services/surfaceflinger/fuzzer/surfaceflinger_layer_fuzzer.cpp
index 921cae4..9f0bdde 100644
--- a/services/surfaceflinger/fuzzer/surfaceflinger_layer_fuzzer.cpp
+++ b/services/surfaceflinger/fuzzer/surfaceflinger_layer_fuzzer.cpp
@@ -106,7 +106,7 @@
     effectLayer->addSurfaceFramePresentedForBuffer(surfaceFrame,
                                                    mFdp.ConsumeIntegral<int64_t>() /*acquireTime*/,
                                                    mFdp.ConsumeIntegral<int64_t>() /*currentTime*/);
-    effectLayer->addSurfaceFrameDroppedForBuffer(surfaceFrame1);
+    effectLayer->addSurfaceFrameDroppedForBuffer(surfaceFrame1, mFdp.ConsumeIntegral<nsecs_t>());
 
     parent.clear();
     client.clear();