[text] TextInputContext implementation, wrapper for TextField implementation

This is the first of two or three changes that implement TextField for
input methods making changes to legacy InputMethodEditorClient text
fields. Once this proxy is complete, the next steps will be:

- Migrate latin-ime and any other input methods to use this new
  TextField interface, their edits still proxied to legacy text fields.
- Bring up a new default input method for physical keyboards, replacing
  the functionality previously hard-coded in Ime.
- Allow Flutter and other clients to implement a TextField and expose it
  to this service, connecting to any input methods.
- Migrate all legacy text fields to the new TextField API.
- Shut down the proxy when no InputMethodEditorClient users remain.

In this change:

- Added conversion from legacy TextInputState into new TextFieldState
- Added functions for creating and accessing TextPoints
- ImeService now also serves a discoverable TextInputContext service
- Ime now also optionally serves a TextField service for input methods,
  although it currently replies to all requests with BAD_REQUEST.
- Added support for running the standard TextField integration test
  suite on the TextField proxy implementation in Ime, although since
  that implementation is still a work in progress, this test is marked
  as `#[ignore]` for now.

Change-Id: Iec302a734c25b6e4daae8d467f9b0545f8361da1
diff --git a/garnet/bin/sysmgr/config/services.config b/garnet/bin/sysmgr/config/services.config
index 3c4258f..f482356 100644
--- a/garnet/bin/sysmgr/config/services.config
+++ b/garnet/bin/sysmgr/config/services.config
@@ -59,6 +59,7 @@
     "fuchsia.ui.input.InputDeviceRegistry": "fuchsia-pkg://fuchsia.com/root_presenter#meta/root_presenter.cmx",
     "fuchsia.ui.policy.Presenter": "fuchsia-pkg://fuchsia.com/root_presenter#meta/root_presenter.cmx",
     "fuchsia.ui.scenic.Scenic": "fuchsia-pkg://fuchsia.com/scenic#meta/scenic.cmx",
+    "fuchsia.ui.text.TextInputContext": "fuchsia-pkg://fuchsia.com/ime_service#meta/ime_service.cmx",
     "fuchsia.ui.sketchy.Canvas": "fuchsia-pkg://fuchsia.com/sketchy_service#meta/sketchy_service.cmx",
     "fuchsia.ui.viewsv1.ViewManager": "fuchsia-pkg://fuchsia.com/view_manager#meta/view_manager.cmx",
     "fuchsia.ui.viewsv1.ViewSnapshot": "fuchsia-pkg://fuchsia.com/view_manager#meta/view_manager.cmx",
diff --git a/garnet/bin/ui/BUILD.gn b/garnet/bin/ui/BUILD.gn
index 2b83e31..b72037e 100644
--- a/garnet/bin/ui/BUILD.gn
+++ b/garnet/bin/ui/BUILD.gn
@@ -165,6 +165,7 @@
 test_package("ime_service_tests") {
   deps = [
     "ime",
+    "//garnet/bin/ui/text/test_suite:test_suite",
   ]
 
   tests = [
diff --git a/garnet/bin/ui/ime/BUILD.gn b/garnet/bin/ui/ime/BUILD.gn
index 72c6462..f4140b6 100644
--- a/garnet/bin/ui/ime/BUILD.gn
+++ b/garnet/bin/ui/ime/BUILD.gn
@@ -10,13 +10,14 @@
   edition = "2018"
 
   deps = [
-    "//sdk/fidl/fuchsia.ui.input:fuchsia.ui.input-rustc",
-    "//sdk/fidl/fuchsia.ui.text:fuchsia.ui.text-rustc",
     "//garnet/public/lib/fidl/rust/fidl",
     "//garnet/public/rust/fuchsia-app",
     "//garnet/public/rust/fuchsia-async",
     "//garnet/public/rust/fuchsia-syslog",
     "//garnet/public/rust/fuchsia-zircon",
+    "//sdk/fidl/fuchsia.ui.input:fuchsia.ui.input-rustc",
+    "//sdk/fidl/fuchsia.ui.text:fuchsia.ui.text-rustc",
+    "//sdk/fidl/fuchsia.ui.text.testing:fuchsia.ui.text.testing-rustc",
     "//third_party/rust_crates:failure",
     "//third_party/rust_crates:futures-preview",
     "//third_party/rust_crates:lazy_static",
diff --git a/garnet/bin/ui/ime/src/ime.rs b/garnet/bin/ui/ime/src/ime.rs
index 92d22048..6b6cdb1 100644
--- a/garnet/bin/ui/ime/src/ime.rs
+++ b/garnet/bin/ui/ime/src/ime.rs
@@ -8,12 +8,14 @@
 use fidl::endpoints::RequestStream;
 use fidl_fuchsia_ui_input as uii;
 use fidl_fuchsia_ui_input::InputMethodEditorRequest as ImeReq;
+use fidl_fuchsia_ui_text as txt;
 use fuchsia_syslog::{fx_log_err, fx_log_warn};
 use futures::prelude::*;
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use regex::Regex;
 use std::char;
+use std::collections::HashMap;
 use std::ops::Range;
 use std::sync::{Arc, Weak};
 use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
@@ -29,10 +31,38 @@
 /// so it can be accessed from multiple places.
 pub struct ImeState {
     text_state: uii::TextInputState,
+
+    /// A handle to call methods on the text field.
     client: Box<uii::InputMethodEditorClientProxyInterface>,
+
     keyboard_type: uii::KeyboardType,
     action: uii::InputMethodAction,
     ime_service: ImeService,
+
+    /// We expose a TextField interface to an input method. There are also legacy
+    /// input methods that just send key events through inject_input — in this case,
+    /// input_method would be None, and these events would be handled by the
+    /// inject_input method. ImeState can only handle talking to one input_method
+    /// at a time; it's the responsibility of some other code (likely inside
+    /// ImeService) to multiplex multiple TextField interfaces into this one.
+    input_method: Option<txt::TextFieldControlHandle>,
+
+    /// A number used to serve the TextField interface. It increments any time any
+    /// party makes a change to the state.
+    revision: u64,
+
+    /// A TextPoint is a u64 token that represents a character position in the new
+    /// TextField interface. Each token is a unique ID; this represents the ID we
+    /// will assign to the next TextPoint that is created. It increments every time
+    /// a TextPoint is created. This is never reset, even when TextPoints are
+    /// invalidated; TextPoints have globally unique IDs.
+    next_text_point_id: u64,
+
+    /// A TextPoint is a u64 token that represents a character position in the new
+    /// TextField interface. This maps TextPoint IDs to byte indexes inside
+    /// `text_state.text`. When a new revision is created, all preexisting TextPoints
+    /// are deleted, which means we clear this out.
+    text_points: HashMap<u64, usize>,
 }
 
 /// A service that talks to a text field, providing it edits and cursor state updates
@@ -54,6 +84,10 @@
             keyboard_type,
             action,
             ime_service,
+            revision: 0,
+            next_text_point_id: 0,
+            text_points: HashMap::new(),
+            input_method: None,
         };
         Ime(Arc::new(Mutex::new(state)))
     }
@@ -66,6 +100,33 @@
         weak.upgrade().map(|arc| Ime(arc))
     }
 
+    pub fn bind_text_field(&self, mut stream: txt::TextFieldRequestStream) {
+        let control_handle = stream.control_handle();
+        {
+            let mut state = self.0.lock();
+            let res = control_handle.send_on_update(&mut state.as_text_field_state());
+            if let Err(e) = res {
+                fx_log_err!("{}", e);
+            } else {
+                state.input_method = Some(control_handle);
+            }
+        }
+        let self_clone = self.clone();
+        fuchsia_async::spawn(
+            async move {
+                while let Some(msg) = await!(stream.try_next())
+                    .context("error reading value from text field request stream")?
+                {
+                    if let Err(e) = self_clone.handle_text_field_msg(msg) {
+                        fx_log_err!("Error when replying to TextFieldRequest: {}", e);
+                    }
+                }
+                Ok(())
+            }
+                .unwrap_or_else(|e: failure::Error| fx_log_err!("{:?}", e)),
+        );
+    }
+
     pub fn bind_ime(&self, chan: fuchsia_async::Channel) {
         let self_clone = self.clone();
         let self_clone_2 = self.clone();
@@ -75,28 +136,7 @@
                 while let Some(msg) = await!(stream.try_next())
                     .context("error reading value from IME request stream")?
                 {
-                    match msg {
-                        ImeReq::SetKeyboardType { keyboard_type, .. } => {
-                            let mut state = self_clone.0.lock();
-                            state.keyboard_type = keyboard_type;
-                        }
-                        ImeReq::SetState { state, .. } => {
-                            self_clone.set_state(state);
-                        }
-                        ImeReq::InjectInput { event, .. } => {
-                            self_clone.inject_input(event);
-                        }
-                        ImeReq::Show { .. } => {
-                            // clone to ensure we only hold one lock at a time
-                            let ime_service = self_clone.0.lock().ime_service.clone();
-                            ime_service.show_keyboard();
-                        }
-                        ImeReq::Hide { .. } => {
-                            // clone to ensure we only hold one lock at a time
-                            let ime_service = self_clone.0.lock().ime_service.clone();
-                            ime_service.hide_keyboard();
-                        }
-                    }
+                    self_clone.handle_ime_message(msg);
                 }
                 Ok(())
             }
@@ -110,9 +150,65 @@
         );
     }
 
+    /// Handles a TextFieldRequest, returning a FIDL error if one occurred when sending a reply.
+    // TODO(lard): finish implementation
+    fn handle_text_field_msg(&self, msg: txt::TextFieldRequest) -> Result<(), fidl::Error> {
+        match msg {
+            txt::TextFieldRequest::PointOffset { responder, .. } => {
+                return responder.send(&mut txt::TextPoint { id: 0 }, txt::TextError::BadRequest);
+            }
+            txt::TextFieldRequest::Distance { responder, .. } => {
+                return responder.send(0, txt::TextError::BadRequest);
+            }
+            txt::TextFieldRequest::Contents { responder, .. } => {
+                return responder.send(
+                    "",
+                    &mut txt::TextPoint { id: 0 },
+                    txt::TextError::BadRequest,
+                );
+            }
+            txt::TextFieldRequest::CommitEdit { responder, .. } => {
+                return responder.send(txt::TextError::BadRequest);
+            }
+            // other cases don't have a responder, so we can just ignore in this temporary code
+            // instead of replying with an error.
+            _ => {
+                return Ok(());
+            }
+        }
+    }
+
+    /// Handles a request from the legancy IME API, an InputMethodEditorRequest.
+    fn handle_ime_message(&self, msg: uii::InputMethodEditorRequest) {
+        match msg {
+            ImeReq::SetKeyboardType { keyboard_type, .. } => {
+                let mut state = self.0.lock();
+                state.keyboard_type = keyboard_type;
+            }
+            ImeReq::SetState { state, .. } => {
+                self.set_state(state);
+            }
+            ImeReq::InjectInput { event, .. } => {
+                self.inject_input(event);
+            }
+            ImeReq::Show { .. } => {
+                // clone to ensure we only hold one lock at a time
+                let ime_service = self.0.lock().ime_service.clone();
+                ime_service.show_keyboard();
+            }
+            ImeReq::Hide { .. } => {
+                // clone to ensure we only hold one lock at a time
+                let ime_service = self.0.lock().ime_service.clone();
+                ime_service.hide_keyboard();
+            }
+        }
+    }
+
     fn set_state(&self, input_state: uii::TextInputState) {
-        self.0.lock().text_state = input_state;
-        // the old C++ IME implementation didn't call did_update_state here, so we won't either.
+        let mut state = self.0.lock();
+        state.text_state = 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);
     }
 
     pub fn inject_input(&self, event: uii::InputEvent) {
@@ -127,24 +223,24 @@
         {
             if keyboard_event.code_point != 0 {
                 state.type_keycode(keyboard_event.code_point);
-                state.did_update_state(keyboard_event)
+                state.increment_revision(Some(keyboard_event), true)
             } else {
                 match keyboard_event.hid_usage {
                     HID_USAGE_KEY_BACKSPACE => {
                         state.delete_backward();
-                        state.did_update_state(keyboard_event);
+                        state.increment_revision(Some(keyboard_event), true);
                     }
                     HID_USAGE_KEY_DELETE => {
                         state.delete_forward();
-                        state.did_update_state(keyboard_event);
+                        state.increment_revision(Some(keyboard_event), true);
                     }
                     HID_USAGE_KEY_LEFT => {
                         state.cursor_horizontal_move(keyboard_event.modifiers, false);
-                        state.did_update_state(keyboard_event);
+                        state.increment_revision(Some(keyboard_event), true);
                     }
                     HID_USAGE_KEY_RIGHT => {
                         state.cursor_horizontal_move(keyboard_event.modifiers, true);
-                        state.did_update_state(keyboard_event);
+                        state.increment_revision(Some(keyboard_event), true);
                     }
                     HID_USAGE_KEY_ENTER => {
                         state.client.on_action(state.action).unwrap_or_else(|e| {
@@ -153,7 +249,7 @@
                     }
                     _ => {
                         // Not an editing key, forward the event to clients.
-                        state.did_update_state(keyboard_event);
+                        state.increment_revision(Some(keyboard_event), true);
                     }
                 }
             }
@@ -191,13 +287,85 @@
 }
 
 impl ImeState {
-    pub fn did_update_state(&mut self, e: uii::KeyboardEvent) {
-        self.client
-            .did_update_state(
-                &mut self.text_state,
-                Some(OutOfLine(&mut uii::InputEvent::Keyboard(e))),
-            )
-            .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,
+    ) {
+        self.revision += 1;
+        self.text_points = HashMap::new();
+        let mut state = self.as_text_field_state();
+        if let Some(input_method) = &self.input_method {
+            if let Err(e) = input_method.send_on_update(&mut state) {
+                fx_log_err!("error when sending update to TextField listener: {}", e);
+            }
+        }
+
+        if call_did_update_state {
+            if let Some(ev) = e {
+                self.client
+                    .did_update_state(
+                        &mut self.text_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 self.text_state, None).unwrap_or_else(|e| {
+                    fx_log_warn!("error sending state update to ImeClient: {:?}", e)
+                });
+            }
+        }
+    }
+
+    /// Converts the current self.text_state (the IME API v1 representation of the text field's state)
+    /// into the v2 representation txt::TextFieldState.
+    fn as_text_field_state(&mut self) -> txt::TextFieldState {
+        let anchor_first = self.text_state.selection.base < self.text_state.selection.extent;
+        txt::TextFieldState {
+            document: txt::TextRange {
+                start: self.new_point(0),
+                end: self.new_point(self.text_state.text.len()),
+            },
+            selection: Some(Box::new(txt::TextSelection {
+                range: txt::TextRange {
+                    start: self.new_point(if anchor_first {
+                        self.text_state.selection.base as usize
+                    } else {
+                        self.text_state.selection.extent as usize
+                    }),
+                    end: self.new_point(if anchor_first {
+                        self.text_state.selection.extent as usize
+                    } else {
+                        self.text_state.selection.base as usize
+                    }),
+                },
+                anchor: if anchor_first {
+                    txt::TextSelectionAnchor::AnchoredAtStart
+                } else {
+                    txt::TextSelectionAnchor::AnchoredAtEnd
+                },
+                affinity: txt::TextAffinity::Upstream,
+            })),
+            // TODO(lard): these three regions should be correctly populated from text_state.
+            composition: None,
+            composition_highlight: None,
+            dead_key_highlight: None,
+            revision: self.revision,
+        }
+    }
+
+    /// Creates a new TextPoint corresponding to the byte index `index`.
+    fn new_point(&mut self, index: usize) -> txt::TextPoint {
+        let id = self.next_text_point_id;
+        self.next_text_point_id += 1;
+        self.text_points.insert(id, index);
+        txt::TextPoint { id }
     }
 
     // gets start and len, and sets base/extent to start of string if don't exist
diff --git a/garnet/bin/ui/ime/src/ime_service.rs b/garnet/bin/ui/ime/src/ime_service.rs
index 9ede804..8b76cf3 100644
--- a/garnet/bin/ui/ime/src/ime_service.rs
+++ b/garnet/bin/ui/ime/src/ime_service.rs
@@ -6,6 +6,7 @@
 use failure::ResultExt;
 use fidl::endpoints::{ClientEnd, RequestStream, ServerEnd};
 use fidl_fuchsia_ui_input as uii;
+use fidl_fuchsia_ui_text as txt;
 use fuchsia_syslog::fx_log_err;
 use futures::prelude::*;
 use parking_lot::Mutex;
@@ -15,6 +16,11 @@
     pub keyboard_visible: bool,
     pub active_ime: Option<Weak<Mutex<ImeState>>>,
     pub visibility_listeners: Vec<uii::ImeVisibilityServiceControlHandle>,
+
+    /// `TextInputContext` is a service provided to input methods that want to edit text. Whenever
+    /// a new text field is focused, we provide a TextField interface to any connected `TextInputContext`s,
+    /// which are listed here.
+    pub text_input_context_clients: Vec<txt::TextInputContextControlHandle>,
 }
 
 /// The internal state of the IMEService, usually held behind an Arc<Mutex>
@@ -43,6 +49,7 @@
             keyboard_visible: false,
             active_ime: None,
             visibility_listeners: Vec::new(),
+            text_input_context_clients: Vec::new(),
         })))
     }
 
@@ -66,7 +73,7 @@
         }
     }
 
-    fn get_input_method_editor(
+    pub fn get_input_method_editor(
         &mut self,
         keyboard_type: uii::KeyboardType,
         action: uii::InputMethodAction,
@@ -74,14 +81,20 @@
         client: ClientEnd<uii::InputMethodEditorClientMarker>,
         editor: ServerEnd<uii::InputMethodEditorMarker>,
     ) {
-        if let Ok(client_proxy) = client.into_proxy() {
-            let ime = Ime::new(keyboard_type, action, initial_state, client_proxy, self.clone());
-            let mut state = self.0.lock();
-            state.active_ime = Some(ime.downgrade());
-            if let Ok(chan) = fuchsia_async::Channel::from_channel(editor.into_channel()) {
-                ime.bind_ime(chan);
-            }
+        let client_proxy = match client.into_proxy() {
+            Ok(v) => v,
+            Err(_) => return,
+        };
+        let ime = Ime::new(keyboard_type, action, initial_state, client_proxy, self.clone());
+        let mut state = self.0.lock();
+        state.active_ime = Some(ime.downgrade());
+        if let Ok(chan) = fuchsia_async::Channel::from_channel(editor.into_channel()) {
+            ime.bind_ime(chan);
         }
+        state.text_input_context_clients.retain(|listener| {
+            // drop listeners if they error on send
+            bind_new_text_field(&ime, &listener).is_ok()
+        });
     }
 
     pub fn show_keyboard(&self) {
@@ -92,7 +105,9 @@
         self.0.lock().update_keyboard_visibility(false);
     }
 
-    fn inject_input(&mut self, event: uii::InputEvent) {
+    /// 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.
+    fn inject_input(&mut self, mut event: uii::InputEvent) {
         let ime = {
             let state = self.0.lock();
             let active_ime_weak = match state.active_ime {
@@ -104,7 +119,15 @@
                 None => return, // IME no longer exists
             }
         };
-        ime.inject_input(event);
+        self.0.lock().text_input_context_clients.retain(|listener| {
+            // drop listeners if they error on send
+            listener.send_on_input_event(&mut event).is_ok()
+        });
+        // only use the default text input handler in ime.rs if there are no text_input_context_clients
+        // attached to handle it
+        if self.0.lock().text_input_context_clients.len() == 0 {
+            ime.inject_input(event);
+        }
     }
 
     pub fn bind_ime_service(&self, chan: fuchsia_async::Channel) {
@@ -167,6 +190,56 @@
                 .unwrap_or_else(|e: failure::Error| fx_log_err!("{:?}", e)),
         );
     }
+
+    pub fn bind_text_input_context(&self, chan: fuchsia_async::Channel) {
+        let self_clone = self.clone();
+        fuchsia_async::spawn(
+            async move {
+                let mut stream = txt::TextInputContextRequestStream::from_channel(chan);
+                let control_handle = stream.control_handle();
+                {
+                    let mut state = self_clone.0.lock();
+
+                    let active_ime_opt = match state.active_ime {
+                        Some(ref weak) => Ime::upgrade(weak),
+                        None => None, // no currently active IME
+                    };
+
+                    if let Some(active_ime) = active_ime_opt {
+                        if let Err(e) = bind_new_text_field(&active_ime, &control_handle) {
+                            fx_log_err!("Error when binding text field for newly connected TextInputContext: {}", e);
+                        }
+                    }
+                    state.text_input_context_clients.push(control_handle)
+                }
+                while let Some(msg) = await!(stream.try_next())
+                    .context("error reading value from text input context request stream")?
+                {
+                    match msg {
+                        txt::TextInputContextRequest::HideKeyboard { .. } => {
+                            self_clone.hide_keyboard();
+                        }
+                    }
+                }
+                Ok(())
+            }
+                .unwrap_or_else(|e: failure::Error| fx_log_err!("{:?}", e)),
+        );
+    }
+}
+
+pub fn bind_new_text_field(
+    active_ime: &Ime,
+    control_handle: &txt::TextInputContextControlHandle,
+) -> Result<(), fidl::Error> {
+    let (client_end, request_stream) =
+        fidl::endpoints::create_request_stream::<txt::TextFieldMarker>()
+            .expect("Failed to create text field request stream");
+    // TODO(lard): this currently overwrites active_ime's TextField, since it only supports
+    // one at a time. In the future, ImeService should multiplex multiple TextField
+    // implementations into Ime.
+    active_ime.bind_text_field(request_stream);
+    control_handle.send_on_focus(client_end)
 }
 
 #[cfg(test)]
diff --git a/garnet/bin/ui/ime/src/main.rs b/garnet/bin/ui/ime/src/main.rs
index e07a1e4..6b55581 100644
--- a/garnet/bin/ui/ime/src/main.rs
+++ b/garnet/bin/ui/ime/src/main.rs
@@ -3,7 +3,6 @@
 // found in the LICENSE file.
 
 #![feature(async_await, await_macro, futures_api)]
-#![deny(warnings)]
 
 mod ime;
 mod ime_service;
@@ -13,6 +12,7 @@
 use failure::{Error, ResultExt};
 use fidl::endpoints::ServiceMarker;
 use fidl_fuchsia_ui_input::{ImeServiceMarker, ImeVisibilityServiceMarker};
+use fidl_fuchsia_ui_text::TextInputContextMarker;
 use fuchsia_app::server::ServicesServer;
 use fuchsia_syslog;
 
@@ -23,6 +23,7 @@
     let ime_service = ime_service::ImeService::new();
     let ime_service1 = ime_service.clone();
     let ime_service2 = ime_service.clone();
+    let ime_service3 = ime_service.clone();
     let done = ServicesServer::new()
         .add_service((ImeServiceMarker::NAME, move |chan| {
             ime_service1.bind_ime_service(chan);
@@ -30,6 +31,9 @@
         .add_service((ImeVisibilityServiceMarker::NAME, move |chan| {
             ime_service2.bind_ime_visibility_service(chan);
         }))
+        .add_service((TextInputContextMarker::NAME, move |chan| {
+            ime_service3.bind_text_input_context(chan);
+        }))
         .start()
         .context("Creating ServicesServer for IME service failed")?;
     executor
@@ -38,3 +42,88 @@
 
     Ok(())
 }
+
+#[cfg(test)]
+mod test {
+    use fidl_fuchsia_ui_input as uii;
+    use fidl_fuchsia_ui_text as txt;
+    use fidl_fuchsia_ui_text_testing as txt_testing;
+    use fuchsia_app::client::Launcher;
+    use futures::prelude::*;
+
+    #[test]
+    // implementation is not complete, so we're ignoring this test for now.
+    #[ignore]
+    fn test_external_text_field_implementation() {
+        fuchsia_syslog::init_with_tags(&["ime_service"]).expect("ime syslog init should not fail");
+        let mut executor = fuchsia_async::Executor::new()
+            .expect("Creating fuchsia_async executor for IME service failed");
+        let launcher = Launcher::new().expect("Failed to open launcher service");
+        let app = launcher
+            .launch(
+                "fuchsia-pkg://fuchsia.com/text_test_suite#meta/test_suite.cmx".to_string(),
+                None,
+            )
+            .expect("Failed to launch testing service");
+        let tester = app
+            .connect_to_service(txt_testing::TextFieldTestSuiteMarker)
+            .expect("Failed to connect to testing service");
+        let done = (async move || {
+            let mut passed = true;
+            let test_list = await!(tester.list_tests()).expect("Failed to get list of tests");
+            for test in test_list {
+                if let Err(e) = await!(run_test(&tester, test.id)) {
+                    passed = false;
+                    eprintln!("[ FAIL ] {}\n{}", test.name, e);
+                } else {
+                    eprintln!("[  ok  ] {}", test.name);
+                }
+            }
+            if !passed {
+                panic!("Text integration tests failed");
+            }
+        })();
+        executor.run_singlethreaded(done);
+    }
+
+    async fn run_test(
+        tester: &txt_testing::TextFieldTestSuiteProxy,
+        test_id: u64,
+    ) -> Result<(), String> {
+        let mut ime_service = crate::ime_service::ImeService::new();
+        let (text_proxy, server_end) =
+            fidl::endpoints::create_proxy::<txt::TextInputContextMarker>()
+                .expect("Failed to create proxy");
+        let (imec_client, _imec_server) =
+            fidl::endpoints::create_endpoints::<uii::InputMethodEditorClientMarker>()
+                .expect("Failed to create endpoints");
+        let (_ime_client, ime_server) =
+            fidl::endpoints::create_endpoints::<uii::InputMethodEditorMarker>()
+                .expect("Failed to create endpoints");
+        let chan = fuchsia_async::Channel::from_channel(server_end.into_channel())
+            .expect("Failed to create channel");
+        ime_service.bind_text_input_context(chan);
+        ime_service.get_input_method_editor(
+            uii::KeyboardType::Text,
+            uii::InputMethodAction::Done,
+            crate::test_helpers::default_state(),
+            imec_client,
+            ime_server,
+        );
+        let mut stream = text_proxy.take_event_stream();
+        let msg = await!(stream.try_next())
+            .expect("Failed to get event.")
+            .expect("TextInputContext event stream unexpectedly closed.");
+        let text_field = match msg {
+            txt::TextInputContextEvent::OnFocus { text_field, .. } => text_field,
+            _ => panic!("Expected text_field to pass OnFocus event type"),
+        };
+        let (passed, msg) = await!(tester.run_test(text_field, test_id))
+            .expect("Call to text testing service failed");
+        if passed {
+            Ok(())
+        } else {
+            Err(msg)
+        }
+    }
+}
diff --git a/garnet/bin/ui/meta/ime_service_unittests.cmx b/garnet/bin/ui/meta/ime_service_unittests.cmx
index 9d3d5aa..727e654 100644
--- a/garnet/bin/ui/meta/ime_service_unittests.cmx
+++ b/garnet/bin/ui/meta/ime_service_unittests.cmx
@@ -4,7 +4,8 @@
     },
     "sandbox": {
         "services": [
-            "fuchsia.logger.LogSink"
+            "fuchsia.logger.LogSink",
+            "fuchsia.sys.Launcher"
         ]
     }
 }