diff --git a/garnet/bin/ui/ime/src/fidl_helpers.rs b/garnet/bin/ui/ime/src/fidl_helpers.rs
index b2243751cf77fe06afe3835d9c2c83b299a81b09..9a623a7d815f4f10bfbcae18e7809058b67eff25 100644
--- a/garnet/bin/ui/ime/src/fidl_helpers.rs
+++ b/garnet/bin/ui/ime/src/fidl_helpers.rs
@@ -26,3 +26,14 @@ pub fn clone_state(state: &uii::TextInputState) -> uii::TextInputState {
         composing: uii::TextRange { start: state.composing.start, end: state.composing.end },
     }
 }
+
+pub fn clone_keyboard_event(ev: &uii::KeyboardEvent) -> uii::KeyboardEvent {
+    uii::KeyboardEvent {
+        event_time: ev.event_time,
+        device_id: ev.device_id,
+        phase: ev.phase,
+        hid_usage: ev.hid_usage,
+        code_point: ev.code_point,
+        modifiers: ev.modifiers,
+    }
+}
diff --git a/garnet/bin/ui/ime/src/ime_service.rs b/garnet/bin/ui/ime/src/ime_service.rs
index dac94321b4155a3ef6eb8069c83eac81c1947e2d..829c0bf85e18bf26e7dde36da0264a54cb08c317 100644
--- a/garnet/bin/ui/ime/src/ime_service.rs
+++ b/garnet/bin/ui/ime/src/ime_service.rs
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+use crate::fidl_helpers::clone_keyboard_event;
 use crate::legacy_ime::Ime;
 use crate::legacy_ime::ImeState;
 use failure::ResultExt;
@@ -109,6 +110,10 @@ impl ImeService {
     /// This is called by the operating system when input from the physical keyboard comes in.
     /// It also is called by legacy onscreen keyboards that just simulate physical keyboard input.
     async fn inject_input(&mut self, mut event: uii::InputEvent) {
+        let keyboard_event = match &event {
+            uii::InputEvent::Keyboard(e) => clone_keyboard_event(e),
+            _ => return,
+        };
         let mut state = await!(self.0.lock());
         let ime = {
             let active_ime_weak = match state.active_ime {
@@ -120,6 +125,10 @@ impl ImeService {
                 None => return, // IME no longer exists
             }
         };
+
+        // send the legacy ime a keystroke event to forward
+        await!(ime.forward_event(clone_keyboard_event(&keyboard_event)));
+
         state.text_input_context_clients.retain(|listener| {
             // drop listeners if they error on send
             listener.send_on_input_event(&mut event).is_ok()
@@ -127,7 +136,7 @@ impl ImeService {
         // only use the default text input handler in ime.rs if there are no text_input_context_clients
         // attached to handle it
         if state.text_input_context_clients.len() == 0 {
-            await!(ime.inject_input(event));
+            await!(ime.inject_input(keyboard_event));
         }
     }
 
@@ -250,6 +259,27 @@ mod test {
     use fuchsia_async as fasync;
     use pin_utils::pin_mut;
 
+    async fn get_state_update(editor_stream: &mut uii::InputMethodEditorClientRequestStream) -> (uii::TextInputState, Option<uii::KeyboardEvent>) {
+        let msg = await!(editor_stream.try_next())
+            .expect("expected working event stream")
+            .expect("ime should have sent message");
+        if let uii::InputMethodEditorClientRequest::DidUpdateState {
+            state, event, ..
+        } = msg
+        {
+            let keyboard_event = event.map(|e| {
+                if let uii::InputEvent::Keyboard(keyboard_event) = *e {
+                    keyboard_event
+                } else {
+                    panic!("expected DidUpdateState to only send Keyboard events");
+                }
+            });
+            (state, keyboard_event)
+        } else {
+            panic!("request should be DidUpdateState");
+        }
+    }
+
     fn async_service_test<T, F>(test_fn: T)
     where
         T: FnOnce(uii::ImeServiceProxy, uii::ImeVisibilityServiceProxy) -> F,
@@ -377,35 +407,53 @@ mod test {
 
                 // type 'a'
                 simulate_keypress(&ime_service, 'a'.into(), 0);
-                let msg = await!(editor_stream.try_next())
-                    .expect("expected working event stream")
-                    .expect("ime should have sent message");
-                if let uii::InputMethodEditorClientRequest::DidUpdateState {
-                    state, event: _, ..
-                } = msg
-                {
-                    assert_eq!(state.text, "a");
-                    assert_eq!(state.selection.base, 1);
-                    assert_eq!(state.selection.extent, 1);
-                } else {
-                    panic!("request should be DidUpdateState");
-                }
+
+                // get first message with keypress event but no state update
+                let (state, event) = await!(get_state_update(&mut editor_stream));
+                let event = event.expect("expected event to be set");
+                assert_eq!(event.phase, uii::KeyboardEventPhase::Pressed);
+                assert_eq!(event.code_point, 97);
+                assert_eq!(state.text, "");
+
+                // get second message with state update
+                let (state, event) = await!(get_state_update(&mut editor_stream));
+                assert!(event.is_none());
+                assert_eq!(state.text, "a");
+                assert_eq!(state.selection.base, 1);
+                assert_eq!(state.selection.extent, 1);
+
+                // get third message with keyrelease event but no state update
+                let (state, event) = await!(get_state_update(&mut editor_stream));
+                let event = event.expect("expected event to be set");
+                assert_eq!(event.phase, uii::KeyboardEventPhase::Released);
+                assert_eq!(event.code_point, 97);
+                assert_eq!(state.text, "a");
 
                 // press left arrow
                 simulate_keypress(&ime_service, 0, HID_USAGE_KEY_LEFT);
-                let msg = await!(editor_stream.try_next())
-                    .expect("expected working event stream")
-                    .expect("ime should have sent message");
-                if let uii::InputMethodEditorClientRequest::DidUpdateState {
-                    state, event: _, ..
-                } = msg
-                {
-                    assert_eq!(state.text, "a");
-                    assert_eq!(state.selection.base, 0);
-                    assert_eq!(state.selection.extent, 0);
-                } else {
-                    panic!("request should be DidUpdateState");
-                }
+
+                // get first message with keypress event but no state update
+                let (state, event) = await!(get_state_update(&mut editor_stream));
+                let event = event.expect("expected event to be set");
+                assert_eq!(event.phase, uii::KeyboardEventPhase::Pressed);
+                assert_eq!(event.code_point, 0);
+                assert_eq!(event.hid_usage, HID_USAGE_KEY_LEFT);
+                assert_eq!(state.text, "a");
+
+                // get second message with state update
+                let (state, event) = await!(get_state_update(&mut editor_stream));
+                assert!(event.is_none());
+                assert_eq!(state.text, "a");
+                assert_eq!(state.selection.base, 0);
+                assert_eq!(state.selection.extent, 0);
+
+                // get first message with keyrelease event but no state update
+                let (state, event) = await!(get_state_update(&mut editor_stream));
+                let event = event.expect("expected event to be set");
+                assert_eq!(event.phase, uii::KeyboardEventPhase::Released);
+                assert_eq!(event.code_point, 0);
+                assert_eq!(event.hid_usage, HID_USAGE_KEY_LEFT);
+                assert_eq!(state.text, "a");
             }
         });
     }
@@ -415,7 +463,18 @@ mod test {
         async_service_test(|ime_service, _visibility_service| {
             async move {
                 let (_ime, mut editor_stream) = bind_ime_for_test(&ime_service);
+
+                // send key events
                 simulate_keypress(&ime_service, 0, HID_USAGE_KEY_ENTER);
+
+                // get first message with keypress event
+                let (_state, event) = await!(get_state_update(&mut editor_stream));
+                let event = event.expect("expected event to be set");
+                assert_eq!(event.phase, uii::KeyboardEventPhase::Pressed);
+                assert_eq!(event.code_point, 0);
+                assert_eq!(event.hid_usage, HID_USAGE_KEY_ENTER);
+
+                // get second message with onaction event
                 let msg = await!(editor_stream.try_next())
                     .expect("expected working event stream")
                     .expect("ime should have sent message");
diff --git a/garnet/bin/ui/ime/src/legacy_ime/handler.rs b/garnet/bin/ui/ime/src/legacy_ime/handler.rs
index 41555ca06d8b4fb20e43581be2945f4916064a9b..934616bb6991458d85676465253dfe592c8f6f3f 100644
--- a/garnet/bin/ui/ime/src/legacy_ime/handler.rs
+++ b/garnet/bin/ui/ime/src/legacy_ime/handler.rs
@@ -201,7 +201,7 @@ impl Ime {
                 }
                 let res = if ime_state.apply_transaction() {
                     let res = responder.send(txt::Error::Ok);
-                    ime_state.increment_revision(None, true);
+                    ime_state.increment_revision(true);
                     res
                 } else {
                     responder.send(txt::Error::BadRequest)
@@ -240,7 +240,11 @@ impl Ime {
                 await!(self.set_state(idx::text_state_codeunit_to_byte(state)));
             }
             ImeReq::InjectInput { event, .. } => {
-                await!(self.inject_input(event));
+                let keyboard_event = match event {
+                    uii::InputEvent::Keyboard(e) => e,
+                    _ => return,
+                };
+                await!(self.inject_input(keyboard_event));
             }
             ImeReq::Show { .. } => {
                 // clone to ensure we only hold one lock at a time
@@ -261,39 +265,40 @@ impl Ime {
         let mut state = await!(self.0.lock());
         state.text_state = idx::text_state_codeunit_to_byte(input_state);
         // the old C++ IME implementation didn't call did_update_state here, so this second argument is false.
-        state.increment_revision(None, false);
+        state.increment_revision(false);
     }
 
-    pub async fn inject_input(&self, event: uii::InputEvent) {
+    pub async fn forward_event(&self, keyboard_event: uii::KeyboardEvent) {
+        let mut state = await!(self.0.lock());
+        state.forward_event(keyboard_event);
+    }
+
+    pub async fn inject_input(&self, keyboard_event: uii::KeyboardEvent) {
         let mut state = await!(self.0.lock());
-        let keyboard_event = match event {
-            uii::InputEvent::Keyboard(e) => e,
-            _ => return,
-        };
 
         if keyboard_event.phase == uii::KeyboardEventPhase::Pressed
             || keyboard_event.phase == uii::KeyboardEventPhase::Repeat
         {
             if keyboard_event.code_point != 0 {
                 state.type_keycode(keyboard_event.code_point);
-                state.increment_revision(Some(keyboard_event), true)
+                state.increment_revision(true)
             } else {
                 match keyboard_event.hid_usage {
                     HID_USAGE_KEY_BACKSPACE => {
                         state.delete_backward();
-                        state.increment_revision(Some(keyboard_event), true);
+                        state.increment_revision(true);
                     }
                     HID_USAGE_KEY_DELETE => {
                         state.delete_forward();
-                        state.increment_revision(Some(keyboard_event), true);
+                        state.increment_revision(true);
                     }
                     HID_USAGE_KEY_LEFT => {
                         state.cursor_horizontal_move(keyboard_event.modifiers, false);
-                        state.increment_revision(Some(keyboard_event), true);
+                        state.increment_revision(true);
                     }
                     HID_USAGE_KEY_RIGHT => {
                         state.cursor_horizontal_move(keyboard_event.modifiers, true);
-                        state.increment_revision(Some(keyboard_event), true);
+                        state.increment_revision(true);
                     }
                     HID_USAGE_KEY_ENTER => {
                         state.client.on_action(state.action).unwrap_or_else(|e| {
@@ -302,7 +307,7 @@ impl Ime {
                     }
                     _ => {
                         // Not an editing key, forward the event to clients.
-                        state.increment_revision(Some(keyboard_event), true);
+                        state.increment_revision(true);
                     }
                 }
             }
diff --git a/garnet/bin/ui/ime/src/legacy_ime/state.rs b/garnet/bin/ui/ime/src/legacy_ime/state.rs
index 091ee598b92a8157084ab1da65bfc68a078b0772..32b648c76c2a973d4b0fc54ef42a21aa43e72260 100644
--- a/garnet/bin/ui/ime/src/legacy_ime/state.rs
+++ b/garnet/bin/ui/ime/src/legacy_ime/state.rs
@@ -88,15 +88,20 @@ pub fn get_range(
 }
 
 impl ImeState {
+    /// Forwards a keyboard event to any listening clients without changing the actual state of the
+    /// IME at all.
+    pub fn forward_event(&mut self, ev: uii::KeyboardEvent) {
+        let mut state = idx::text_state_byte_to_codeunit(clone_state(&self.text_state));
+        self.client
+            .did_update_state(&mut state, Some(OutOfLine(&mut uii::InputEvent::Keyboard(ev))))
+            .unwrap_or_else(|e| fx_log_warn!("error sending state update to ImeClient: {:?}", e));
+    }
+
     /// Any time the state is updated, this method is called, which allows ImeState to inform any
     /// listening clients (either TextField or InputMethodEditorClientProxy) that state has updated.
     /// If InputMethodEditorClient caused the update with SetState, set call_did_update_state so that
     /// we don't send its own edit back to it. Otherwise, set to true.
-    pub fn increment_revision(
-        &mut self,
-        e: Option<uii::KeyboardEvent>,
-        call_did_update_state: bool,
-    ) {
+    pub fn increment_revision(&mut self, call_did_update_state: bool) {
         self.revision += 1;
         self.text_points = HashMap::new();
         let state = self.as_text_field_state();
@@ -108,20 +113,9 @@ impl ImeState {
 
         if call_did_update_state {
             let mut state = idx::text_state_byte_to_codeunit(clone_state(&self.text_state));
-            if let Some(ev) = e {
-                self.client
-                    .did_update_state(
-                        &mut state,
-                        Some(OutOfLine(&mut uii::InputEvent::Keyboard(ev))),
-                    )
-                    .unwrap_or_else(|e| {
-                        fx_log_warn!("error sending state update to ImeClient: {:?}", e)
-                    });
-            } else {
-                self.client.did_update_state(&mut state, None).unwrap_or_else(|e| {
-                    fx_log_warn!("error sending state update to ImeClient: {:?}", e)
-                });
-            }
+            self.client.did_update_state(&mut state, None).unwrap_or_else(|e| {
+                fx_log_warn!("error sending state update to ImeClient: {:?}", e)
+            });
         }
     }
 
diff --git a/garnet/bin/ui/ime/src/legacy_ime/tests.rs b/garnet/bin/ui/ime/src/legacy_ime/tests.rs
index f174a12e81f095e7bf6a061a88ddcd99d7827cde..a2a6d498020676b6fea9a6f4d45ff9528e1d4371 100644
--- a/garnet/bin/ui/ime/src/legacy_ime/tests.rs
+++ b/garnet/bin/ui/ime/src/legacy_ime/tests.rs
@@ -48,22 +48,22 @@ async fn simulate_keypress<K: Into<u32> + Copy + 'static>(
 ) {
     let hid_usage = if hid_key { key.into() } else { 0 };
     let code_point = if hid_key { 0 } else { key.into() };
-    await!(ime.inject_input(uii::InputEvent::Keyboard(uii::KeyboardEvent {
+    await!(ime.inject_input(uii::KeyboardEvent {
         event_time: 0,
         device_id: 0,
         phase: uii::KeyboardEventPhase::Pressed,
         hid_usage,
         code_point,
         modifiers,
-    })));
-    await!(ime.inject_input(uii::InputEvent::Keyboard(uii::KeyboardEvent {
+    }));
+    await!(ime.inject_input(uii::KeyboardEvent {
         event_time: 0,
         device_id: 0,
         phase: uii::KeyboardEventPhase::Released,
         hid_usage,
         code_point,
         modifiers,
-    })));
+    }));
 }
 
 struct MockImeClient {