Re-create XRInputSource when handedness or target ray mode changes.

Also fire an inputsourceschange event when this happens. This behavior
is required by the WebXR spec.

The browser test verifies that changing the XRInputSource handedness
causes it to be re-created while firing the appropriate events.

The layout test uses MockXRInputSource and verifies for both handedness
and target ray mode attributes that changing them causes the
XRInputSource to be re-created while firing the appropriate events.

Bug: 958019
Change-Id: Ifdca807536e3c8116f9dc7fb3dbfb15f8e946c37
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1616442
Reviewed-by: Alexander Cooper <alcooper@chromium.org>
Reviewed-by: Bill Orr <billorr@chromium.org>
Commit-Queue: Jacob DeWitt <jacde@chromium.org>
Cr-Commit-Position: refs/heads/master@{#661438}
diff --git a/chrome/browser/vr/webxr_vr_input_browser_test.cc b/chrome/browser/vr/webxr_vr_input_browser_test.cc
index c7f45eb..75e9215 100644
--- a/chrome/browser/vr/webxr_vr_input_browser_test.cc
+++ b/chrome/browser/vr/webxr_vr_input_browser_test.cc
@@ -168,6 +168,13 @@
     UpdateControllerAndWait(controller_index, controller_data);
   }
 
+  void UpdateControllerRole(unsigned int controller_index,
+                            device::ControllerRole role) {
+    auto controller_data = GetCurrentControllerData(controller_index);
+    controller_data.role = role;
+    UpdateControllerAndWait(controller_index, controller_data);
+  }
+
  private:
   vr::EVRButtonId GetAxisId(unsigned int offset) {
     return static_cast<vr::EVRButtonId>(vr::k_EButton_Axis0 + offset);
@@ -200,6 +207,42 @@
   std::move(callback).Run();
 }
 
+// Ensure that when an input source's handedness changes, an input source change
+// event is fired and a new input source is created.
+IN_PROC_BROWSER_TEST_F(WebXrVrBrowserTestStandard, TestInputHandednessChange) {
+  WebXrControllerInputMock my_mock;
+  unsigned int controller_index = my_mock.CreateAndConnectMinimalGamepad();
+
+  LoadUrlAndAwaitInitialization(
+      GetFileUrlForHtmlTestFile("test_webxr_input_same_object"));
+  EnterSessionWithUserGestureOrFail();
+
+  // We should only have seen the first change indicating we have input sources.
+  PollJavaScriptBooleanOrFail("inputChangeEvents === 1", kPollTimeoutShort);
+
+  // We only expect one input source, cache it.
+  RunJavaScriptOrFail("validateInputSourceLength(1)");
+  RunJavaScriptOrFail("updateCachedInputSource(0)");
+
+  // Change the handedness from right to left and verify that we get a change
+  // event.  Then cache the new input source.
+  my_mock.UpdateControllerRole(controller_index,
+                               device::ControllerRole::kControllerRoleLeft);
+  PollJavaScriptBooleanOrFail("inputChangeEvents === 2", kPollTimeoutShort);
+  RunJavaScriptOrFail("validateCachedSourcePresence(false)");
+  RunJavaScriptOrFail("validateInputSourceLength(1)");
+  RunJavaScriptOrFail("updateCachedInputSource(0)");
+
+  // Switch back to the right hand and confirm that we get the change.
+  my_mock.UpdateControllerRole(controller_index,
+                               device::ControllerRole::kControllerRoleRight);
+  PollJavaScriptBooleanOrFail("inputChangeEvents === 3", kPollTimeoutShort);
+  RunJavaScriptOrFail("validateCachedSourcePresence(false)");
+  RunJavaScriptOrFail("validateInputSourceLength(1)");
+  RunJavaScriptOrFail("done()");
+  EndTest();
+}
+
 // Test that inputsourceschange events contain only the expected added/removed
 // input sources when a mock controller is connected/disconnected.
 // Also validates that if an input source changes substantially we get an event
diff --git a/third_party/blink/renderer/modules/xr/xr_canvas_input_provider.cc b/third_party/blink/renderer/modules/xr/xr_canvas_input_provider.cc
index 472afd8d..df2cf3f 100644
--- a/third_party/blink/renderer/modules/xr/xr_canvas_input_provider.cc
+++ b/third_party/blink/renderer/modules/xr/xr_canvas_input_provider.cc
@@ -93,8 +93,8 @@
     return;
 
   if (!input_source_) {
-    input_source_ = MakeGarbageCollected<XRInputSource>(session_, 0);
-    input_source_->SetTargetRayMode(XRInputSource::kScreen);
+    input_source_ = MakeGarbageCollected<XRInputSource>(session_, 0,
+                                                        XRInputSource::kScreen);
   }
 
   // Get the event location relative to the canvas element.
diff --git a/third_party/blink/renderer/modules/xr/xr_input_source.cc b/third_party/blink/renderer/modules/xr/xr_input_source.cc
index 630d929..6146d0e 100644
--- a/third_party/blink/renderer/modules/xr/xr_input_source.cc
+++ b/third_party/blink/renderer/modules/xr/xr_input_source.cc
@@ -79,6 +79,8 @@
     const device::mojom::blink::XRInputSourceDescriptionPtr& desc =
         state->description;
 
+    // Setting target ray mode and handedness is fine here because earlier in
+    // this function the input source was re-created if necessary.
     updated_source->SetTargetRayMode(
         MojomToBlinkTargetRayMode(desc->target_ray_mode));
     updated_source->SetHandedness(MojomToBlinkHandedness(desc->handedness));
@@ -106,20 +108,22 @@
   return updated_source;
 }
 
-XRInputSource::XRInputSource(XRSession* session, uint32_t source_id)
+XRInputSource::XRInputSource(XRSession* session,
+                             uint32_t source_id,
+                             TargetRayMode target_ray_mode)
     : session_(session),
       source_id_(source_id),
       target_ray_space_(MakeGarbageCollected<XRTargetRaySpace>(session, this)),
       grip_space_(MakeGarbageCollected<XRGripSpace>(session, this)),
       base_timestamp_(session->xr()->NavigationStart()) {
-  SetTargetRayMode(kGaze);
+  SetTargetRayMode(target_ray_mode);
   SetHandedness(kHandNone);
 }
 
 XRInputSource::XRInputSource(
     XRSession* session,
     const device::mojom::blink::XRInputSourceStatePtr& state)
-    : XRInputSource(session, state->source_id) {
+    : XRInputSource(session, state->source_id, kGaze) {
   if (state->gamepad) {
     gamepad_ = MakeGarbageCollected<Gamepad>(this, 0, base_timestamp_,
                                              TimeTicks::Now());
@@ -176,6 +180,20 @@
     return true;
   }
 
+  if (state->description) {
+    Handedness other_handedness =
+        MojomToBlinkHandedness(state->description->handedness);
+    if (other_handedness != handedness_) {
+      return true;
+    }
+
+    TargetRayMode other_mode =
+        MojomToBlinkTargetRayMode(state->description->target_ray_mode);
+    if (other_mode != target_ray_mode_) {
+      return true;
+    }
+  }
+
   return false;
 }
 
diff --git a/third_party/blink/renderer/modules/xr/xr_input_source.h b/third_party/blink/renderer/modules/xr/xr_input_source.h
index f1b545e2..9704436 100644
--- a/third_party/blink/renderer/modules/xr/xr_input_source.h
+++ b/third_party/blink/renderer/modules/xr/xr_input_source.h
@@ -43,7 +43,7 @@
       XRSession* session,
       const device::mojom::blink::XRInputSourceStatePtr& state);
 
-  XRInputSource(XRSession*, uint32_t source_id);
+  XRInputSource(XRSession*, uint32_t source_id, TargetRayMode);
   XRInputSource(XRSession*,
                 const device::mojom::blink::XRInputSourceStatePtr& state);
   XRInputSource(const XRInputSource& other);
@@ -60,7 +60,6 @@
 
   uint32_t source_id() const { return source_id_; }
 
-  void SetTargetRayMode(TargetRayMode);
   void SetPointerTransformMatrix(std::unique_ptr<TransformationMatrix>);
 
   // Gamepad::Client
@@ -83,6 +82,7 @@
   friend class XRTargetRaySpace;
 
   void SetHandedness(Handedness);
+  void SetTargetRayMode(TargetRayMode);
   void SetEmulatedPosition(bool emulated_position);
   void SetBasePoseMatrix(std::unique_ptr<TransformationMatrix>);
 
diff --git a/third_party/blink/web_tests/xr/events_input_source_recreation.html b/third_party/blink/web_tests/xr/events_input_source_recreation.html
new file mode 100644
index 0000000..db44c52
--- /dev/null
+++ b/third_party/blink/web_tests/xr/events_input_source_recreation.html
@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<script src="../resources/testharness.js"></script>
+<script src="../resources/testharnessreport.js"></script>
+<script src="file:///gen/layout_test_data/mojo/public/js/mojo_bindings.js"></script>
+<script src="file:///gen/device/vr/public/mojom/vr_service.mojom.js"></script>
+<script src="../external/wpt/resources/chromium/webxr-test.js"></script>
+<script src="../external/wpt/webxr/resources/webxr_test_constants.js"></script>
+<script src="../xr/resources/xr-internal-device-mocking.js"></script>
+<script src="../xr/resources/xr-test-utils.js"></script>
+<canvas id="webgl-canvas"></canvas>
+
+<script>
+let testName = "Input sources are re-created when handedness or target ray mode changes";
+
+let watcherDone = new Event("watcherdone");
+
+let fakeDeviceInitParams = { supportsImmersive:true };
+
+let requestSessionModes = ['immersive-vr'];
+
+let testFunction = function(session, t, fakeDeviceController) {
+  let eventWatcher = new EventWatcher(t, session, ["watcherdone"]);
+  let eventPromise = eventWatcher.wait_for(["watcherdone"]);
+
+  // Need to have a valid pose or input events don't process.
+  fakeDeviceController.setXRPresentationFrameData(VALID_POSE_MATRIX, [{
+      eye:"left",
+      projectionMatrix: VALID_PROJECTION_MATRIX,
+      viewMatrix: VALID_VIEW_MATRIX
+    }, {
+      eye:"right",
+      projectionMatrix: VALID_PROJECTION_MATRIX,
+      viewMatrix: VALID_VIEW_MATRIX
+    }]);
+
+  let inputChangeEvents = 0;
+  let cached_input_source = null;
+  function onInputSourcesChange(event) {
+    t.step(() => {
+      inputChangeEvents++;
+      assert_equals(event.session, session);
+
+      if (inputChangeEvents == 1) {
+        // The first change event should be adding our controller.
+        validateAdded(event.added, 1);
+        validateRemoved(event.removed, 0);
+        cached_input_source = getInputSources()[0];
+        assert_not_equals(cached_input_source, null);
+        assert_equals(cached_input_source.handedness, "none");
+        assert_equals(cached_input_source.targetRayMode, "gaze");
+      } else if (inputChangeEvents == 2) {
+        // The second event should be replacing the controller with one that has
+        // the updated target ray mode.
+        validateInputSourceChange(event, "none", "tracked-pointer");
+        cached_input_source = getInputSources()[0];
+      } else if (inputChangeEvents == 3) {
+        // The third event should be replacing the controller with one that has
+        // the updated handedness.
+        validateInputSourceChange(event, "left", "tracked-pointer");
+        session.dispatchEvent(watcherDone);
+      }
+    });
+  }
+
+  function validateInputSourceChange(event, expected_hand, expected_mode) {
+    validateAdded(event.added, 1);
+    validateRemoved(event.removed, 1);
+    assert_true(event.removed.includes(cached_input_source));
+    assert_false(event.added.includes(cached_input_source));
+    let source = event.added[0];
+    assert_equals(source.handedness, expected_hand);
+    assert_equals(source.targetRayMode, expected_mode);
+  }
+
+  function validateAdded(added, length) {
+    t.step(() => {
+      assert_not_equals(added, null);
+      assert_equals(added.length, length,
+          "Added length matches expectations");
+
+      let currentSources = getInputSources();
+      added.forEach((source) => {
+        assert_true(currentSources.includes(source),
+          "Every element in added should be in the input source list");
+      });
+    });
+  }
+
+  function validateRemoved(removed, length) {
+    t.step(() => {
+      assert_not_equals(removed, null);
+        assert_equals(removed.length, length,
+            "Removed length matches expectations");
+
+      let currentSources = getInputSources();
+      removed.forEach((source) => {
+        assert_false(currentSources.includes(source),
+          "No element in removed should be in the input source list");
+      });
+    });
+  }
+
+  function getInputSources() {
+    return Array.from(session.inputSources.values());
+  }
+
+  session.addEventListener('inputsourceschange', onInputSourcesChange, false);
+
+  // Session must have a baseLayer or frame requests will be ignored.
+  session.updateRenderState({ baseLayer: new XRWebGLLayer(session, gl) });
+
+  // Create our input source and immediately toggle the primary input so that
+  // it appears as already needing to send a click event when it appears.
+  let input_source = new MockXRInputSource();
+  fakeDeviceController.addInputSource(input_source);
+
+  // Make our input source change after one frame, and wait an additional
+  // frame for that change to propogate.
+  session.requestAnimationFrame((time, xrFrame) => {
+    input_source.targetRayMode = "tracked-pointer";
+    session.requestAnimationFrame((time, xrFrame) => {
+      input_source.handedness = "left";
+      session.requestAnimationFrame((time, xrFrame) => {});
+    });
+  });
+
+  return eventPromise;
+};
+
+xr_session_promise_test(
+  testFunction, fakeDeviceInitParams, requestSessionModes, testName);
+</script>