Replace ash::HighlighterControllerObserver with a mojo client

This makes metalayer/highlighter features fully mash-compatible.

Bug: 769994

Test: ash_unittests --gtest_filter=Palette*
Change-Id: I8344ffdd246357cc14866cf19dbfcc9644be00e5
Reviewed-on: https://chromium-review.googlesource.com/693346
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Reviewed-by: Luis Hector Chavez <lhchavez@chromium.org>
Reviewed-by: David Reveman <reveman@chromium.org>
Reviewed-by: James Cook <jamescook@chromium.org>
Commit-Queue: Vladislav Kaznacheev <kaznacheev@chromium.org>
Cr-Commit-Position: refs/heads/master@{#508366}
diff --git a/ash/BUILD.gn b/ash/BUILD.gn
index a78721d..51a0ae3 100644
--- a/ash/BUILD.gn
+++ b/ash/BUILD.gn
@@ -189,7 +189,6 @@
     "highlighter/highlighter_gesture_util.h",
     "highlighter/highlighter_result_view.cc",
     "highlighter/highlighter_result_view.h",
-    "highlighter/highlighter_selection_observer.h",
     "highlighter/highlighter_view.cc",
     "highlighter/highlighter_view.h",
     "host/ash_window_tree_host.cc",
diff --git a/ash/fast_ink/fast_ink_pointer_controller.h b/ash/fast_ink/fast_ink_pointer_controller.h
index 74e1f2f..4aad6e2 100644
--- a/ash/fast_ink/fast_ink_pointer_controller.h
+++ b/ash/fast_ink/fast_ink_pointer_controller.h
@@ -27,6 +27,8 @@
   FastInkPointerController();
   ~FastInkPointerController() override;
 
+  bool enabled() const { return enabled_; }
+
   // Enables/disables the pointer. The user still has to press to see
   // the pointer.
   virtual void SetEnabled(bool enabled);
diff --git a/ash/fast_ink/fast_ink_view.cc b/ash/fast_ink/fast_ink_view.cc
index 1ad83ce..da3d1b7 100644
--- a/ash/fast_ink/fast_ink_view.cc
+++ b/ash/fast_ink/fast_ink_view.cc
@@ -37,6 +37,11 @@
 struct FastInkView::Resource {
   Resource() {}
   ~Resource() {
+    // context_provider might be null in unit tests when ran with --mash
+    // TODO(kaznacheev) Have MASH provide a context provider for tests
+    // when crbug/772562 is fixed
+    if (!context_provider)
+      return;
     gpu::gles2::GLES2Interface* gles2 = context_provider->ContextGL();
     if (sync_token.HasData())
       gles2->WaitSyncTokenCHROMIUM(sync_token.GetConstData());
diff --git a/ash/highlighter/highlighter_controller.cc b/ash/highlighter/highlighter_controller.cc
index 9a10077..98e6eba 100644
--- a/ash/highlighter/highlighter_controller.cc
+++ b/ash/highlighter/highlighter_controller.cc
@@ -8,7 +8,6 @@
 
 #include "ash/highlighter/highlighter_gesture_util.h"
 #include "ash/highlighter/highlighter_result_view.h"
-#include "ash/highlighter/highlighter_selection_observer.h"
 #include "ash/highlighter/highlighter_view.h"
 #include "ash/public/cpp/scale_utility.h"
 #include "base/metrics/histogram_macros.h"
@@ -52,15 +51,11 @@
 
 }  // namespace
 
-HighlighterController::HighlighterController() {}
+HighlighterController::HighlighterController()
+    : binding_(this), weak_factory_(this) {}
 
 HighlighterController::~HighlighterController() {}
 
-void HighlighterController::SetObserver(
-    HighlighterSelectionObserver* observer) {
-  observer_ = observer;
-}
-
 void HighlighterController::SetExitCallback(base::OnceClosure exit_callback,
                                             bool require_success) {
   exit_callback_ = std::move(exit_callback);
@@ -86,8 +81,21 @@
     if (highlighter_view_ && !highlighter_view_->animating())
       DestroyPointerView();
   }
-  if (observer_)
-    observer_->HandleEnabledStateChange(enabled);
+  if (client_)
+    client_->HandleEnabledStateChange(enabled);
+}
+
+void HighlighterController::BindRequest(
+    mojom::HighlighterControllerRequest request) {
+  binding_.Bind(std::move(request));
+}
+
+void HighlighterController::SetClient(
+    mojom::HighlighterControllerClientPtr client) {
+  client_ = std::move(client);
+  client_.set_connection_error_handler(
+      base::Bind(&HighlighterController::OnClientConnectionLost,
+                 weak_factory_.GetWeakPtr()));
 }
 
 views::View* HighlighterController::GetPointerView() const {
@@ -174,8 +182,8 @@
 
   if (!box.IsEmpty() &&
       gesture_type != HighlighterGestureType::kNotRecognized) {
-    if (observer_) {
-      observer_->HandleSelection(gfx::ToEnclosingRect(
+    if (client_) {
+      client_->HandleSelection(gfx::ToEnclosingRect(
           gfx::ScaleRect(box, GetScreenshotScale(current_window))));
     }
 
@@ -187,8 +195,6 @@
     recognized_gesture_counter_++;
     CallExitCallback();
   } else {
-    if (observer_)
-      observer_->HandleFailedSelection();
     if (!require_success_)
       CallExitCallback();
   }
@@ -233,9 +239,21 @@
   result_view_.reset();
 }
 
+void HighlighterController::OnClientConnectionLost() {
+  client_.reset();
+  binding_.Close();
+  // The client has detached, force-exit the highlighter mode.
+  CallExitCallback();
+}
+
 void HighlighterController::CallExitCallback() {
   if (!exit_callback_.is_null())
     std::move(exit_callback_).Run();
 }
 
+void HighlighterController::FlushMojoForTesting() {
+  if (client_)
+    client_.FlushForTesting();
+}
+
 }  // namespace ash
diff --git a/ash/highlighter/highlighter_controller.h b/ash/highlighter/highlighter_controller.h
index c7a340e..5ef8c4e 100644
--- a/ash/highlighter/highlighter_controller.h
+++ b/ash/highlighter/highlighter_controller.h
@@ -8,7 +8,10 @@
 #include <memory>
 
 #include "ash/fast_ink/fast_ink_pointer_controller.h"
+#include "ash/public/interfaces/highlighter_controller.mojom.h"
 #include "base/callback.h"
+#include "base/memory/weak_ptr.h"
+#include "mojo/public/cpp/bindings/binding.h"
 
 namespace base {
 class OneShotTimer;
@@ -17,20 +20,17 @@
 namespace ash {
 
 class HighlighterResultView;
-class HighlighterSelectionObserver;
 class HighlighterView;
 
 // Controller for the highlighter functionality.
 // Enables/disables highlighter as well as receives points
 // and passes them off to be rendered.
-class ASH_EXPORT HighlighterController : public FastInkPointerController {
+class ASH_EXPORT HighlighterController : public FastInkPointerController,
+                                         public mojom::HighlighterController {
  public:
   HighlighterController();
   ~HighlighterController() override;
 
-  // Set the observer to handle selection results.
-  void SetObserver(HighlighterSelectionObserver* observer);
-
   // Set the callback to exit the highlighter mode. If |require_success| is
   // true, the callback will be called only after a successful gesture
   // recognition. If |require_success| is false, the callback will be  called
@@ -40,6 +40,11 @@
   // FastInkPointerController:
   void SetEnabled(bool enabled) override;
 
+  void BindRequest(mojom::HighlighterControllerRequest request);
+
+  // mojom::HighlighterController:
+  void SetClient(mojom::HighlighterControllerClientPtr client) override;
+
  private:
   friend class HighlighterControllerTestApi;
 
@@ -61,9 +66,14 @@
   // Destroys |result_view_|, if it exists.
   void DestroyResultView();
 
+  // Called when the mojo connection with the client is closed.
+  void OnClientConnectionLost();
+
   // Calls and clears the mode exit callback, if it is set.
   void CallExitCallback();
 
+  void FlushMojoForTesting();
+
   // |highlighter_view_| will only hold an instance when the highlighter is
   // enabled and activated (pressed or dragged) and until the fade out
   // animation is done.
@@ -73,9 +83,6 @@
   // animation is in progress.
   std::unique_ptr<HighlighterResultView> result_view_;
 
-  // |observer_| is not owned by the controller.
-  HighlighterSelectionObserver* observer_ = nullptr;
-
   // Time of the session start (e.g. when the controller was enabled).
   base::TimeTicks session_start_;
 
@@ -99,6 +106,14 @@
   // If true, the mode is not exited until a valid selection is made.
   bool require_success_ = true;
 
+  // Binding for mojom::HighlighterController interface.
+  mojo::Binding<ash::mojom::HighlighterController> binding_;
+
+  // Interface to highlighter controller client (chrome).
+  mojom::HighlighterControllerClientPtr client_;
+
+  base::WeakPtrFactory<HighlighterController> weak_factory_;
+
   DISALLOW_COPY_AND_ASSIGN(HighlighterController);
 };
 
diff --git a/ash/highlighter/highlighter_controller_test_api.cc b/ash/highlighter/highlighter_controller_test_api.cc
index 47e9649..e10f4c1 100644
--- a/ash/highlighter/highlighter_controller_test_api.cc
+++ b/ash/highlighter/highlighter_controller_test_api.cc
@@ -12,19 +12,34 @@
 
 HighlighterControllerTestApi::HighlighterControllerTestApi(
     HighlighterController* instance)
-    : instance_(instance) {
-  instance_->SetObserver(this);
+    : binding_(this), instance_(instance) {
+  AttachClient();
 }
 
 HighlighterControllerTestApi::~HighlighterControllerTestApi() {
-  instance_->SetObserver(nullptr);
-  if (enabled_)
+  if (binding_.is_bound())
+    DetachClient();
+  if (instance_->enabled())
     instance_->SetEnabled(false);
   instance_->DestroyPointerView();
 }
 
-void HighlighterControllerTestApi::CallMetalayerDone() {
-  instance_->CallExitCallback();
+void HighlighterControllerTestApi::AttachClient() {
+  DCHECK(!binding_.is_bound());
+  DCHECK(!highlighter_controller_);
+  instance_->BindRequest(mojo::MakeRequest(&highlighter_controller_));
+  ash::mojom::HighlighterControllerClientPtr client;
+  binding_.Bind(mojo::MakeRequest(&client));
+  highlighter_controller_->SetClient(std::move(client));
+  highlighter_controller_.FlushForTesting();
+}
+
+void HighlighterControllerTestApi::DetachClient() {
+  DCHECK(binding_.is_bound());
+  DCHECK(highlighter_controller_);
+  highlighter_controller_ = nullptr;
+  binding_.Close();
+  instance_->FlushMojoForTesting();
 }
 
 void HighlighterControllerTestApi::SetEnabled(bool enabled) {
@@ -67,15 +82,21 @@
   return instance_->highlighter_view_->predicted_points_;
 }
 
+bool HighlighterControllerTestApi::HandleEnabledStateChangedCalled() {
+  instance_->FlushMojoForTesting();
+  return handle_enabled_state_changed_called_;
+}
+
+bool HighlighterControllerTestApi::HandleSelectionCalled() {
+  instance_->FlushMojoForTesting();
+  return handle_selection_called_;
+}
+
 void HighlighterControllerTestApi::HandleSelection(const gfx::Rect& rect) {
   handle_selection_called_ = true;
   selection_ = rect;
 }
 
-void HighlighterControllerTestApi::HandleFailedSelection() {
-  handle_failed_selection_called_ = true;
-}
-
 void HighlighterControllerTestApi::HandleEnabledStateChange(bool enabled) {
   handle_enabled_state_changed_called_ = true;
   enabled_ = enabled;
diff --git a/ash/highlighter/highlighter_controller_test_api.h b/ash/highlighter/highlighter_controller_test_api.h
index 78331b3..67c9dc2 100644
--- a/ash/highlighter/highlighter_controller_test_api.h
+++ b/ash/highlighter/highlighter_controller_test_api.h
@@ -5,8 +5,9 @@
 #ifndef ASH_HIGHLIGHTER_HIGHLIGHTER_CONTROLLER_TEST_API_H_
 #define ASH_HIGHLIGHTER_HIGHLIGHTER_CONTROLLER_TEST_API_H_
 
-#include "ash/highlighter/highlighter_selection_observer.h"
+#include "ash/public/interfaces/highlighter_controller.mojom.h"
 #include "base/macros.h"
+#include "mojo/public/cpp/bindings/binding.h"
 #include "ui/gfx/geometry/rect.h"
 
 namespace ash {
@@ -15,14 +16,22 @@
 class HighlighterController;
 
 // An api for testing the HighlighterController class.
-// Inheriting from HighlighterSelectionObserver to provide the tests
-// with access to gesture recognition results.
-class HighlighterControllerTestApi : public HighlighterSelectionObserver {
+// Implements ash::mojom::HighlighterControllerClient and binds itself as the
+// client to provide the tests with access to gesture recognition results.
+class HighlighterControllerTestApi
+    : public ash::mojom::HighlighterControllerClient {
  public:
   explicit HighlighterControllerTestApi(HighlighterController* instance);
   ~HighlighterControllerTestApi() override;
 
-  void CallMetalayerDone();
+  // Attaches itself as the client to the controller. This method is called
+  // automatically from the constructor, and should be explicitly called only
+  // if DetachClient has been called previously.
+  void AttachClient();
+
+  // Detaches itself from the controller.
+  void DetachClient();
+
   void SetEnabled(bool enabled);
   void DestroyPointerView();
   void SimulateInterruptedStrokeTimeout();
@@ -34,31 +43,31 @@
   const FastInkPoints& predicted_points() const;
 
   void ResetEnabledState() { handle_enabled_state_changed_called_ = false; }
-  bool handle_enabled_state_changed_called() const {
-    return handle_enabled_state_changed_called_;
-  }
+  // Flushes the mojo connection, then checks whether HandleEnabledStateChange
+  // has been called on the client since the last call to ResetEnabledState.
+  bool HandleEnabledStateChangedCalled();
   bool enabled() const { return enabled_; }
 
-  void ResetSelection() {
-    handle_selection_called_ = false;
-    handle_failed_selection_called_ = false;
-  }
-  bool handle_selection_called() const { return handle_selection_called_; }
-  bool handle_failed_selection_called() const {
-    return handle_failed_selection_called_;
-  }
+  void ResetSelection() { handle_selection_called_ = false; }
+  // Flushes the mojo connection, then checks whether HandleSelection
+  // has been called on the client since the last call to ResetSelection.
+  bool HandleSelectionCalled();
   const gfx::Rect& selection() const { return selection_; }
 
  private:
   // HighlighterSelectionObserver:
   void HandleSelection(const gfx::Rect& rect) override;
-  void HandleFailedSelection() override;
   void HandleEnabledStateChange(bool enabled) override;
 
+  // Binds to the client interface.
+  mojo::Binding<ash::mojom::HighlighterControllerClient> binding_;
+
+  // HighlighterController interface.
+  ash::mojom::HighlighterControllerPtr highlighter_controller_;
+
   HighlighterController* instance_;
 
   bool handle_selection_called_ = false;
-  bool handle_failed_selection_called_ = false;
   bool handle_enabled_state_changed_called_ = false;
   gfx::Rect selection_;
   bool enabled_ = false;
diff --git a/ash/highlighter/highlighter_controller_unittest.cc b/ash/highlighter/highlighter_controller_unittest.cc
index 69e73f3..28f653f 100644
--- a/ash/highlighter/highlighter_controller_unittest.cc
+++ b/ash/highlighter/highlighter_controller_unittest.cc
@@ -166,8 +166,7 @@
   GetEventGenerator().PressTouch();
   GetEventGenerator().MoveTouch(gfx::Point(200, 200));
   GetEventGenerator().ReleaseTouch();
-  EXPECT_FALSE(controller_test_api_->handle_selection_called());
-  EXPECT_TRUE(controller_test_api_->handle_failed_selection_called());
+  EXPECT_FALSE(controller_test_api_->HandleSelectionCalled());
 
   // An almost horizontal stroke is recognized
   controller_test_api_->ResetSelection();
@@ -175,8 +174,7 @@
   GetEventGenerator().PressTouch();
   GetEventGenerator().MoveTouch(gfx::Point(300, 102));
   GetEventGenerator().ReleaseTouch();
-  EXPECT_TRUE(controller_test_api_->handle_selection_called());
-  EXPECT_FALSE(controller_test_api_->handle_failed_selection_called());
+  EXPECT_TRUE(controller_test_api_->HandleSelectionCalled());
 
   // Horizontal stroke selection rectangle should:
   //   have the same horizontal center line as the stroke bounding box,
@@ -192,8 +190,7 @@
   GetEventGenerator().MoveTouch(gfx::Point(0, 100));
   GetEventGenerator().MoveTouch(gfx::Point(100, 100));
   GetEventGenerator().ReleaseTouch();
-  EXPECT_FALSE(controller_test_api_->handle_selection_called());
-  EXPECT_TRUE(controller_test_api_->handle_failed_selection_called());
+  EXPECT_FALSE(controller_test_api_->HandleSelectionCalled());
 
   // An almost closed G-like shape is recognized
   controller_test_api_->ResetSelection();
@@ -204,8 +201,7 @@
   GetEventGenerator().MoveTouch(gfx::Point(200, 100));
   GetEventGenerator().MoveTouch(gfx::Point(200, 20));
   GetEventGenerator().ReleaseTouch();
-  EXPECT_TRUE(controller_test_api_->handle_selection_called());
-  EXPECT_FALSE(controller_test_api_->handle_failed_selection_called());
+  EXPECT_TRUE(controller_test_api_->HandleSelectionCalled());
   EXPECT_EQ("0,0 200x100", controller_test_api_->selection().ToString());
 
   // A closed diamond shape is recognized
@@ -217,8 +213,7 @@
   GetEventGenerator().MoveTouch(gfx::Point(0, 150));
   GetEventGenerator().MoveTouch(gfx::Point(100, 50));
   GetEventGenerator().ReleaseTouch();
-  EXPECT_TRUE(controller_test_api_->handle_selection_called());
-  EXPECT_FALSE(controller_test_api_->handle_failed_selection_called());
+  EXPECT_TRUE(controller_test_api_->HandleSelectionCalled());
   EXPECT_EQ("0,50 200x200", controller_test_api_->selection().ToString());
 }
 
@@ -249,8 +244,7 @@
 
       controller_test_api_->ResetSelection();
       TraceRect(original_rect);
-      EXPECT_TRUE(controller_test_api_->handle_selection_called());
-      EXPECT_FALSE(controller_test_api_->handle_failed_selection_called());
+      EXPECT_TRUE(controller_test_api_->HandleSelectionCalled());
 
       const gfx::Rect selection = controller_test_api_->selection();
       EXPECT_TRUE(inflated.Contains(selection));
@@ -270,32 +264,28 @@
   UpdateDisplay("1500x1000");
   controller_test_api_->ResetSelection();
   TraceRect(trace);
-  EXPECT_TRUE(controller_test_api_->handle_selection_called());
-  EXPECT_FALSE(controller_test_api_->handle_failed_selection_called());
+  EXPECT_TRUE(controller_test_api_->HandleSelectionCalled());
   EXPECT_EQ("200,100 400x300", controller_test_api_->selection().ToString());
 
   // Rotate to 90 degrees
   UpdateDisplay("1500x1000/r");
   controller_test_api_->ResetSelection();
   TraceRect(trace);
-  EXPECT_TRUE(controller_test_api_->handle_selection_called());
-  EXPECT_FALSE(controller_test_api_->handle_failed_selection_called());
+  EXPECT_TRUE(controller_test_api_->HandleSelectionCalled());
   EXPECT_EQ("100,900 300x400", controller_test_api_->selection().ToString());
 
   // Rotate to 180 degrees
   UpdateDisplay("1500x1000/u");
   controller_test_api_->ResetSelection();
   TraceRect(trace);
-  EXPECT_TRUE(controller_test_api_->handle_selection_called());
-  EXPECT_FALSE(controller_test_api_->handle_failed_selection_called());
+  EXPECT_TRUE(controller_test_api_->HandleSelectionCalled());
   EXPECT_EQ("900,600 400x300", controller_test_api_->selection().ToString());
 
   // Rotate to 270 degrees
   UpdateDisplay("1500x1000/l");
   controller_test_api_->ResetSelection();
   TraceRect(trace);
-  EXPECT_TRUE(controller_test_api_->handle_selection_called());
-  EXPECT_FALSE(controller_test_api_->handle_failed_selection_called());
+  EXPECT_TRUE(controller_test_api_->HandleSelectionCalled());
   EXPECT_EQ("600,200 300x400", controller_test_api_->selection().ToString());
 }
 
@@ -315,8 +305,7 @@
   GetEventGenerator().MoveTouch(gfx::Point(0, 100));
   GetEventGenerator().ReleaseTouch();
   EXPECT_TRUE(controller_test_api_->IsWaitingToResumeStroke());
-  EXPECT_FALSE(controller_test_api_->handle_selection_called());
-  EXPECT_FALSE(controller_test_api_->handle_failed_selection_called());
+  EXPECT_FALSE(controller_test_api_->HandleSelectionCalled());
   EXPECT_FALSE(controller_test_api_->IsFadingAway());
 
   GetEventGenerator().MoveTouch(gfx::Point(0, 200));
@@ -324,8 +313,7 @@
   GetEventGenerator().MoveTouch(gfx::Point(300, 200));
   GetEventGenerator().ReleaseTouch();
   EXPECT_FALSE(controller_test_api_->IsWaitingToResumeStroke());
-  EXPECT_TRUE(controller_test_api_->handle_selection_called());
-  EXPECT_FALSE(controller_test_api_->handle_failed_selection_called());
+  EXPECT_TRUE(controller_test_api_->HandleSelectionCalled());
   EXPECT_EQ("0,100 300x100", controller_test_api_->selection().ToString());
 
   // Repeat the same gesture, but simulate a timeout after the gap. This should
@@ -336,14 +324,12 @@
   GetEventGenerator().MoveTouch(gfx::Point(0, 100));
   GetEventGenerator().ReleaseTouch();
   EXPECT_TRUE(controller_test_api_->IsWaitingToResumeStroke());
-  EXPECT_FALSE(controller_test_api_->handle_selection_called());
-  EXPECT_FALSE(controller_test_api_->handle_failed_selection_called());
+  EXPECT_FALSE(controller_test_api_->HandleSelectionCalled());
   EXPECT_FALSE(controller_test_api_->IsFadingAway());
 
   controller_test_api_->SimulateInterruptedStrokeTimeout();
   EXPECT_FALSE(controller_test_api_->IsWaitingToResumeStroke());
-  EXPECT_TRUE(controller_test_api_->handle_selection_called());
-  EXPECT_FALSE(controller_test_api_->handle_failed_selection_called());
+  EXPECT_TRUE(controller_test_api_->HandleSelectionCalled());
   EXPECT_TRUE(controller_test_api_->IsFadingAway());
 }
 
@@ -366,34 +352,34 @@
     controller_test_api_->ResetSelection();
     TraceRect(gfx::Rect(-100, -100, 10, 10));
     controller_test_api_->SimulateInterruptedStrokeTimeout();
-    EXPECT_TRUE(controller_test_api_->handle_failed_selection_called());
+    EXPECT_FALSE(controller_test_api_->HandleSelectionCalled());
 
     // Rectangle crossing the left edge.
     controller_test_api_->ResetSelection();
     TraceRect(gfx::Rect(-100, 100, 200, 200));
     controller_test_api_->SimulateInterruptedStrokeTimeout();
-    EXPECT_TRUE(controller_test_api_->handle_selection_called());
+    EXPECT_TRUE(controller_test_api_->HandleSelectionCalled());
     EXPECT_TRUE(screen.Contains(controller_test_api_->selection()));
 
     // Rectangle crossing the top edge.
     controller_test_api_->ResetSelection();
     TraceRect(gfx::Rect(100, -100, 200, 200));
     controller_test_api_->SimulateInterruptedStrokeTimeout();
-    EXPECT_TRUE(controller_test_api_->handle_selection_called());
+    EXPECT_TRUE(controller_test_api_->HandleSelectionCalled());
     EXPECT_TRUE(screen.Contains(controller_test_api_->selection()));
 
     // Rectangle crossing the right edge.
     controller_test_api_->ResetSelection();
     TraceRect(gfx::Rect(900, 100, 200, 200));
     controller_test_api_->SimulateInterruptedStrokeTimeout();
-    EXPECT_TRUE(controller_test_api_->handle_selection_called());
+    EXPECT_TRUE(controller_test_api_->HandleSelectionCalled());
     EXPECT_TRUE(screen.Contains(controller_test_api_->selection()));
 
     // Rectangle crossing the bottom edge.
     controller_test_api_->ResetSelection();
     TraceRect(gfx::Rect(100, 900, 200, 200));
     controller_test_api_->SimulateInterruptedStrokeTimeout();
-    EXPECT_TRUE(controller_test_api_->handle_selection_called());
+    EXPECT_TRUE(controller_test_api_->HandleSelectionCalled());
     EXPECT_TRUE(screen.Contains(controller_test_api_->selection()));
 
     // Horizontal stroke completely offscreen.
@@ -403,7 +389,7 @@
     GetEventGenerator().MoveTouch(gfx::Point(1000, -100));
     GetEventGenerator().ReleaseTouch();
     controller_test_api_->SimulateInterruptedStrokeTimeout();
-    EXPECT_TRUE(controller_test_api_->handle_failed_selection_called());
+    EXPECT_FALSE(controller_test_api_->HandleSelectionCalled());
 
     // Horizontal stroke along the top edge of the screen.
     controller_test_api_->ResetSelection();
@@ -412,7 +398,7 @@
     GetEventGenerator().MoveTouch(gfx::Point(1000, 0));
     GetEventGenerator().ReleaseTouch();
     controller_test_api_->SimulateInterruptedStrokeTimeout();
-    EXPECT_TRUE(controller_test_api_->handle_selection_called());
+    EXPECT_TRUE(controller_test_api_->HandleSelectionCalled());
     EXPECT_TRUE(screen.Contains(controller_test_api_->selection()));
 
     // Horizontal stroke along the bottom edge of the screen.
@@ -422,9 +408,44 @@
     GetEventGenerator().MoveTouch(gfx::Point(1000, 999));
     GetEventGenerator().ReleaseTouch();
     controller_test_api_->SimulateInterruptedStrokeTimeout();
-    EXPECT_TRUE(controller_test_api_->handle_selection_called());
+    EXPECT_TRUE(controller_test_api_->HandleSelectionCalled());
     EXPECT_TRUE(screen.Contains(controller_test_api_->selection()));
   }
 }
 
+// Test that a detached client does not receive notifications.
+TEST_F(HighlighterControllerTest, DetachedClient) {
+  controller_test_api_->SetEnabled(true);
+  GetEventGenerator().EnterPenPointerMode();
+
+  UpdateDisplay("1500x1000");
+  const gfx::Rect trace(200, 100, 400, 300);
+
+  // Detach the client, no notifications should reach it.
+  controller_test_api_->DetachClient();
+
+  controller_test_api_->ResetEnabledState();
+  controller_test_api_->SetEnabled(false);
+  EXPECT_FALSE(controller_test_api_->HandleEnabledStateChangedCalled());
+  controller_test_api_->SetEnabled(true);
+  EXPECT_FALSE(controller_test_api_->HandleEnabledStateChangedCalled());
+
+  controller_test_api_->ResetSelection();
+  TraceRect(trace);
+  EXPECT_FALSE(controller_test_api_->HandleSelectionCalled());
+
+  // Attach the client again, notifications should be delivered normally.
+  controller_test_api_->AttachClient();
+
+  controller_test_api_->ResetEnabledState();
+  controller_test_api_->SetEnabled(false);
+  EXPECT_TRUE(controller_test_api_->HandleEnabledStateChangedCalled());
+  controller_test_api_->SetEnabled(true);
+  EXPECT_TRUE(controller_test_api_->HandleEnabledStateChangedCalled());
+
+  controller_test_api_->ResetSelection();
+  TraceRect(trace);
+  EXPECT_TRUE(controller_test_api_->HandleSelectionCalled());
+}
+
 }  // namespace ash
diff --git a/ash/highlighter/highlighter_selection_observer.h b/ash/highlighter/highlighter_selection_observer.h
deleted file mode 100644
index e8fafee..0000000
--- a/ash/highlighter/highlighter_selection_observer.h
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright 2017 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef ASH_HIGHLIGHTER_HIGHLIGHTER_SELECTION_OBSERVER_H_
-#define ASH_HIGHLIGHTER_HIGHLIGHTER_SELECTION_OBSERVER_H_
-
-namespace gfx {
-class Rect;
-}  // namespace gfx
-
-namespace ash {
-
-// Observer for handling highlighter selection.
-class HighlighterSelectionObserver {
- public:
-  virtual ~HighlighterSelectionObserver() {}
-
-  // |rect| is the selected rectangle in screen pixes, clipped to screen bounds
-  // if necessary.
-  virtual void HandleSelection(const gfx::Rect& rect) = 0;
-  virtual void HandleFailedSelection() = 0;
-  virtual void HandleEnabledStateChange(bool enabled) = 0;
-};
-
-}  // namespace ash
-
-#endif  // ASH_HIGHLIGHTER_HIGHLIGHTER_SELECTION_OBSERVER_H_
diff --git a/ash/mojo_interface_factory.cc b/ash/mojo_interface_factory.cc
index e2daf45..393031b 100644
--- a/ash/mojo_interface_factory.cc
+++ b/ash/mojo_interface_factory.cc
@@ -9,6 +9,7 @@
 #include "ash/accelerators/accelerator_controller.h"
 #include "ash/cast_config_controller.h"
 #include "ash/display/ash_display_controller.h"
+#include "ash/highlighter/highlighter_controller.h"
 #include "ash/ime/ime_controller.h"
 #include "ash/login/lock_screen_controller.h"
 #include "ash/media_controller.h"
@@ -57,6 +58,11 @@
   Shell::Get()->cast_config()->BindRequest(std::move(request));
 }
 
+void BindHighlighterControllerRequestOnMainThread(
+    mojom::HighlighterControllerRequest request) {
+  Shell::Get()->highlighter_controller()->BindRequest(std::move(request));
+}
+
 void BindImeControllerRequestOnMainThread(mojom::ImeControllerRequest request) {
   Shell::Get()->ime_controller()->BindRequest(std::move(request));
 }
@@ -132,14 +138,17 @@
       main_thread_task_runner);
   registry->AddInterface(base::Bind(&BindAppListRequestOnMainThread),
                          main_thread_task_runner);
-  registry->AddInterface(base::Bind(&BindImeControllerRequestOnMainThread),
-                         main_thread_task_runner);
   registry->AddInterface(
       base::Bind(&BindAshDisplayControllerRequestOnMainThread),
       main_thread_task_runner);
   registry->AddInterface(base::Bind(&BindCastConfigOnMainThread),
                          main_thread_task_runner);
   registry->AddInterface(
+      base::Bind(&BindHighlighterControllerRequestOnMainThread),
+      main_thread_task_runner);
+  registry->AddInterface(base::Bind(&BindImeControllerRequestOnMainThread),
+                         main_thread_task_runner);
+  registry->AddInterface(
       base::Bind(&BindLocaleNotificationControllerOnMainThread),
       main_thread_task_runner);
   registry->AddInterface(base::Bind(&BindLockScreenRequestOnMainThread),
diff --git a/ash/mus/manifest.json b/ash/mus/manifest.json
index 93605e1..7b30464 100644
--- a/ash/mus/manifest.json
+++ b/ash/mus/manifest.json
@@ -11,6 +11,7 @@
           "app_list::mojom::AppList",
           "ash::mojom::AcceleratorController",
           "ash::mojom::CastConfig",
+          "ash::mojom::HighlighterController",
           "ash::mojom::ImeController",
           "ash::mojom::LocaleNotificationController",
           "ash::mojom::LockScreen",
diff --git a/ash/mus/standalone/manifest.json b/ash/mus/standalone/manifest.json
index 12c66fe..a939a786 100644
--- a/ash/mus/standalone/manifest.json
+++ b/ash/mus/standalone/manifest.json
@@ -8,6 +8,7 @@
           "app_list::mojom::AppList",
           "ash::mojom::AcceleratorController",
           "ash::mojom::CastConfig",
+          "ash::mojom::HighlighterController",
           "ash::mojom::ImeController",
           "ash::mojom::LocaleNotificationController",
           "ash::mojom::MediaController",
diff --git a/ash/public/interfaces/BUILD.gn b/ash/public/interfaces/BUILD.gn
index cc8517e6..7b49da5 100644
--- a/ash/public/interfaces/BUILD.gn
+++ b/ash/public/interfaces/BUILD.gn
@@ -17,6 +17,7 @@
     "cast_config.mojom",
     "constants.mojom",
     "event_properties.mojom",
+    "highlighter_controller.mojom",
     "ime_controller.mojom",
     "ime_info.mojom",
     "locale.mojom",
diff --git a/ash/public/interfaces/highlighter_controller.mojom b/ash/public/interfaces/highlighter_controller.mojom
new file mode 100644
index 0000000..fc679f5
--- /dev/null
+++ b/ash/public/interfaces/highlighter_controller.mojom
@@ -0,0 +1,26 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+module ash.mojom;
+
+import "ui/gfx/geometry/mojo/geometry.mojom";
+
+// Interface for ash client (e.g. Chrome) to connect to the highlighter
+// controller, the component implementing on-screen content selection
+// with a stylus.
+interface HighlighterController {
+  // Sets the client interface.
+  SetClient(HighlighterControllerClient client);
+};
+
+// Interface for ash to notify the client (e.g. Chrome) about the highlighter
+// selection and state.
+interface HighlighterControllerClient {
+  // Called when when a valid selection is made. Selected rectangle is in
+  // screen coordinates, clipped to screen bounds if necessary.
+  HandleSelection(gfx.mojom.Rect rect);
+
+  // Called when the highlighter tool becomes enabled or disabled.
+  HandleEnabledStateChange(bool enabled);
+};
diff --git a/ash/system/palette/palette_tray_unittest.cc b/ash/system/palette/palette_tray_unittest.cc
index 37f2b49..c25ba75 100644
--- a/ash/system/palette/palette_tray_unittest.cc
+++ b/ash/system/palette/palette_tray_unittest.cc
@@ -336,11 +336,7 @@
                          true /* highlighter shown on press */);
   // When metalayer is entered normally (not via stylus button), a failed
   // selection should not exit the mode.
-  // NOTE that this is not testing the real logic in PaletteDelegateChromeOS,
-  // but the logic in HighlighterControllerTestApi (which is mimicking
-  // PaletteDelegateChromeOS). Once PaletteDelegateChromeOS is refactored
-  // (crbug/761120) the assertions below become more useful.
-  EXPECT_TRUE(highlighter_test_api_->handle_failed_selection_called());
+  EXPECT_FALSE(highlighter_test_api_->HandleSelectionCalled());
   EXPECT_TRUE(metalayer_enabled());
 
   // A successfull selection should exit the metalayer mode.
@@ -351,7 +347,7 @@
   EXPECT_TRUE(metalayer_enabled());
   generator.MoveTouch(gfx::Point(300, 100));
   generator.ReleaseTouch();
-  EXPECT_TRUE(highlighter_test_api_->handle_selection_called());
+  EXPECT_TRUE(highlighter_test_api_->HandleSelectionCalled());
   EXPECT_FALSE(metalayer_enabled());
 
   SCOPED_TRACE("drag over palette");
diff --git a/ash/system/palette/tools/metalayer_unittest.cc b/ash/system/palette/tools/metalayer_unittest.cc
index caf5df6..22dd114 100644
--- a/ash/system/palette/tools/metalayer_unittest.cc
+++ b/ash/system/palette/tools/metalayer_unittest.cc
@@ -102,27 +102,27 @@
   }
 }
 
-// Verifies that enabling/disabling the metalayer tool invokes the delegate.
+// Verifies that the metalayer enabled/disabled state propagates to the
+// highlighter controller.
 TEST_F(MetalayerToolTest, EnablingDisablingMetalayerEnablesDisablesController) {
   // Enabling the metalayer tool enables the highligher controller.
   // It should also hide the palette.
   EXPECT_CALL(*palette_tool_delegate_.get(), HidePalette());
   highlighter_test_api_->ResetEnabledState();
   tool_->OnEnable();
-  EXPECT_TRUE(highlighter_test_api_->handle_enabled_state_changed_called());
+  EXPECT_TRUE(highlighter_test_api_->HandleEnabledStateChangedCalled());
   EXPECT_TRUE(highlighter_test_api_->enabled());
   testing::Mock::VerifyAndClearExpectations(palette_tool_delegate_.get());
 
   // Disabling the metalayer tool disables the highlighter controller.
   highlighter_test_api_->ResetEnabledState();
   tool_->OnDisable();
-  EXPECT_TRUE(highlighter_test_api_->handle_enabled_state_changed_called());
+  EXPECT_TRUE(highlighter_test_api_->HandleEnabledStateChangedCalled());
   EXPECT_FALSE(highlighter_test_api_->enabled());
   testing::Mock::VerifyAndClearExpectations(palette_tool_delegate_.get());
 }
 
-// Verifies that disabling the metalayer support in the delegate disables the
-// tool.
+// Verifies that disabling the metalayer support disables the tool.
 TEST_F(MetalayerToolTest, MetalayerUnsupportedDisablesPaletteTool) {
   Shell::Get()->NotifyVoiceInteractionStatusChanged(
       VoiceInteractionState::RUNNING);
@@ -166,13 +166,21 @@
   testing::Mock::VerifyAndClearExpectations(palette_tool_delegate_.get());
 }
 
-// Verifies that invoking the callback passed to the delegate disables the tool.
-TEST_F(MetalayerToolTest, MetalayerCallbackDisablesPaletteTool) {
+// Verifies that detaching the highlighter client disables the palette tool.
+TEST_F(MetalayerToolTest, DetachingClientDisablesPaletteTool) {
   tool_->OnEnable();
-  // Calling the associated callback |metalayer_done| will disable the tool.
+  // If the client detaches, the tool should become disabled.
   EXPECT_CALL(*palette_tool_delegate_.get(),
               DisableTool(PaletteToolId::METALAYER));
-  highlighter_test_api_->CallMetalayerDone();
+  highlighter_test_api_->DetachClient();
+  testing::Mock::VerifyAndClearExpectations(palette_tool_delegate_.get());
+
+  // If the client attaches again, the tool should not become enabled.
+  highlighter_test_api_->AttachClient();
+  EXPECT_CALL(*palette_tool_delegate_.get(),
+              EnableTool(PaletteToolId::METALAYER))
+      .Times(0);
+  testing::Mock::VerifyAndClearExpectations(palette_tool_delegate_.get());
 }
 
 }  // namespace ash
diff --git a/chrome/browser/chromeos/arc/voice_interaction/arc_voice_interaction_framework_service.cc b/chrome/browser/chromeos/arc/voice_interaction/arc_voice_interaction_framework_service.cc
index 4659910..9f48fa7 100644
--- a/chrome/browser/chromeos/arc/voice_interaction/arc_voice_interaction_framework_service.cc
+++ b/chrome/browser/chromeos/arc/voice_interaction/arc_voice_interaction_framework_service.cc
@@ -195,6 +195,7 @@
     : context_(context),
       arc_bridge_service_(bridge_service),
       binding_(this),
+      highlighter_client_(std::make_unique<HighlighterControllerClient>(this)),
       weak_ptr_factory_(this) {
   arc_bridge_service_->voice_interaction_framework()->AddObserver(this);
   ArcSessionManager::Get()->AddObserver(this);
@@ -228,12 +229,13 @@
     }
   }
 
-  highlighter_client_ = std::make_unique<HighlighterControllerClient>(this);
+  highlighter_client_->Attach();
 }
 
 void ArcVoiceInteractionFrameworkService::OnInstanceClosed() {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
-  highlighter_client_.reset();
+  binding_.Close();
+  highlighter_client_->Detach();
 }
 
 void ArcVoiceInteractionFrameworkService::CaptureFullscreen(
diff --git a/chrome/browser/chromeos/arc/voice_interaction/arc_voice_interaction_framework_service.h b/chrome/browser/chromeos/arc/voice_interaction/arc_voice_interaction_framework_service.h
index a2a2204..3b08ddb 100644
--- a/chrome/browser/chromeos/arc/voice_interaction/arc_voice_interaction_framework_service.h
+++ b/chrome/browser/chromeos/arc/voice_interaction/arc_voice_interaction_framework_service.h
@@ -123,6 +123,10 @@
   // Starts voice interaction OOBE flow.
   void StartVoiceInteractionOobe();
 
+  HighlighterControllerClient* GetHighlighterClientForTesting() const {
+    return highlighter_client_.get();
+  }
+
   // For supporting ArcServiceManager::GetService<T>().
   static const char kArcServiceName[];
 
diff --git a/chrome/browser/chromeos/arc/voice_interaction/arc_voice_interaction_framework_service_unittest.cc b/chrome/browser/chromeos/arc/voice_interaction/arc_voice_interaction_framework_service_unittest.cc
index 696c6fa..c249863 100644
--- a/chrome/browser/chromeos/arc/voice_interaction/arc_voice_interaction_framework_service_unittest.cc
+++ b/chrome/browser/chromeos/arc/voice_interaction/arc_voice_interaction_framework_service_unittest.cc
@@ -11,6 +11,7 @@
 #include "base/bind.h"
 #include "base/files/scoped_temp_dir.h"
 #include "chrome/browser/chromeos/arc/arc_session_manager.h"
+#include "chrome/browser/chromeos/arc/voice_interaction/highlighter_controller_client.h"
 #include "chrome/test/base/testing_profile.h"
 #include "chromeos/dbus/dbus_thread_manager.h"
 #include "chromeos/dbus/fake_cras_audio_client.h"
@@ -21,10 +22,54 @@
 #include "components/prefs/pref_service.h"
 #include "components/session_manager/core/session_manager.h"
 #include "testing/gtest/include/gtest/gtest.h"
-#include "ui/gfx/geometry/rect.h"
 
 namespace arc {
 
+namespace {
+
+class TestHighlighterController : public ash::mojom::HighlighterController {
+ public:
+  TestHighlighterController() : binding_(this), weak_factory_(this) {}
+  ~TestHighlighterController() override = default;
+
+  ash::mojom::HighlighterControllerPtr CreateInterfacePtrAndBind() {
+    ash::mojom::HighlighterControllerPtr ptr;
+    binding_.Bind(mojo::MakeRequest(&ptr));
+    return ptr;
+  }
+
+  void CallHandleSelection(const gfx::Rect& rect) {
+    client_->HandleSelection(rect);
+  }
+
+  void CallHandleEnabledStateChange(bool enabled) {
+    client_->HandleEnabledStateChange(enabled);
+  }
+
+  bool client_attached() const { return static_cast<bool>(client_); }
+
+  // ash::mojom::HighlighterController:
+  void SetClient(ash::mojom::HighlighterControllerClientPtr client) override {
+    DCHECK(!client_);
+    client_ = std::move(client);
+    client_.set_connection_error_handler(
+        base::Bind(&TestHighlighterController::OnClientConnectionLost,
+                   weak_factory_.GetWeakPtr()));
+  }
+
+  void OnClientConnectionLost() { client_.reset(); }
+
+ private:
+  mojo::Binding<ash::mojom::HighlighterController> binding_;
+  ash::mojom::HighlighterControllerClientPtr client_;
+
+  base::WeakPtrFactory<TestHighlighterController> weak_factory_;
+
+  DISALLOW_COPY_AND_ASSIGN(TestHighlighterController);
+};
+
+}  // namespace
+
 class ArcVoiceInteractionFrameworkServiceTest : public ash::AshTestBase {
  public:
   ArcVoiceInteractionFrameworkServiceTest() = default;
@@ -44,12 +89,19 @@
     arc_session_manager_ = std::make_unique<ArcSessionManager>(
         std::make_unique<ArcSessionRunner>(base::Bind(FakeArcSession::Create)));
     arc_bridge_service_ = std::make_unique<ArcBridgeService>();
+    highlighter_controller_ = std::make_unique<TestHighlighterController>();
     framework_service_ = std::make_unique<ArcVoiceInteractionFrameworkService>(
         profile_.get(), arc_bridge_service_.get());
+    framework_service_->GetHighlighterClientForTesting()
+        ->SetControllerForTesting(
+            highlighter_controller_->CreateInterfacePtrAndBind());
     framework_instance_ =
         std::make_unique<FakeVoiceInteractionFrameworkInstance>();
     arc_bridge_service_->voice_interaction_framework()->SetInstance(
         framework_instance_.get());
+    // Flushing is required for the AttachClient call to get through to the
+    // highligther controller.
+    FlushHighlighterControllerMojo();
 
     framework_service()->SetVoiceInteractionSetupCompleted();
   }
@@ -75,12 +127,21 @@
     return framework_instance_.get();
   }
 
+  TestHighlighterController* highlighter_controller() {
+    return highlighter_controller_.get();
+  }
+
+  void FlushHighlighterControllerMojo() {
+    framework_service_->GetHighlighterClientForTesting()->FlushMojoForTesting();
+  }
+
  private:
   base::ScopedTempDir temp_dir_;
   std::unique_ptr<TestingProfile> profile_;
   std::unique_ptr<session_manager::SessionManager> session_manager_;
   std::unique_ptr<ArcBridgeService> arc_bridge_service_;
   std::unique_ptr<ArcSessionManager> arc_session_manager_;
+  std::unique_ptr<TestHighlighterController> highlighter_controller_;
   std::unique_ptr<ArcVoiceInteractionFrameworkService> framework_service_;
   std::unique_ptr<FakeVoiceInteractionFrameworkInstance> framework_instance_;
 
@@ -137,4 +198,71 @@
   EXPECT_TRUE(framework_service()->ValidateTimeSinceUserInteraction());
 }
 
+TEST_F(ArcVoiceInteractionFrameworkServiceTest, HighlighterControllerClient) {
+  EXPECT_TRUE(highlighter_controller()->client_attached());
+
+  // Enabled state should propagate to the framework instance.
+  highlighter_controller()->CallHandleEnabledStateChange(true);
+  FlushHighlighterControllerMojo();
+  EXPECT_EQ(1u, framework_instance()->set_metalayer_visibility_count());
+  EXPECT_TRUE(framework_instance()->metalayer_visible());
+
+  // Disabled state should propagate to the framework instance.
+  framework_instance()->ResetCounters();
+  highlighter_controller()->CallHandleEnabledStateChange(false);
+  FlushHighlighterControllerMojo();
+  EXPECT_EQ(1u, framework_instance()->set_metalayer_visibility_count());
+  EXPECT_FALSE(framework_instance()->metalayer_visible());
+
+  // Enable the state again.
+  framework_instance()->ResetCounters();
+  highlighter_controller()->CallHandleEnabledStateChange(true);
+  FlushHighlighterControllerMojo();
+  EXPECT_EQ(1u, framework_instance()->set_metalayer_visibility_count());
+  EXPECT_TRUE(framework_instance()->metalayer_visible());
+
+  // Simulate a valid selection.
+  framework_instance()->ResetCounters();
+  const gfx::Rect selection(100, 200, 300, 400);
+  highlighter_controller()->CallHandleSelection(selection);
+  highlighter_controller()->CallHandleEnabledStateChange(false);
+  FlushHighlighterControllerMojo();
+  // Neither the selected region nor the state update should reach the
+  // framework instance yet.
+  EXPECT_EQ(0u, framework_instance()->start_session_for_region_count());
+  EXPECT_EQ(0u, framework_instance()->set_metalayer_visibility_count());
+  EXPECT_TRUE(framework_instance()->metalayer_visible());
+  framework_service()
+      ->GetHighlighterClientForTesting()
+      ->SimulateSelectionTimeoutForTesting();
+  FlushHighlighterControllerMojo();
+  // After a timeout, the selected region should reach the framework instance.
+  EXPECT_EQ(1u, framework_instance()->start_session_for_region_count());
+  EXPECT_EQ(selection.ToString(),
+            framework_instance()->selected_region().ToString());
+  // However, the state update should not be explicitly sent to the framework
+  // instance, since the state change is implied with a valid selection.
+  EXPECT_EQ(0u, framework_instance()->set_metalayer_visibility_count());
+
+  // Clear the framework instance to simulate the container crash.
+  // The client should become detached.
+  arc_bridge_service()->voice_interaction_framework()->SetInstance(nullptr);
+  FlushHighlighterControllerMojo();
+  EXPECT_FALSE(highlighter_controller()->client_attached());
+
+  // Set the framework instance again to simulate the container restart.
+  // The client should become attached again.
+  arc_bridge_service()->voice_interaction_framework()->SetInstance(
+      framework_instance());
+  FlushHighlighterControllerMojo();
+  EXPECT_TRUE(highlighter_controller()->client_attached());
+
+  // State update should reach the client normally.
+  framework_instance()->ResetCounters();
+  highlighter_controller()->CallHandleEnabledStateChange(true);
+  FlushHighlighterControllerMojo();
+  EXPECT_EQ(1u, framework_instance()->set_metalayer_visibility_count());
+  EXPECT_TRUE(framework_instance()->metalayer_visible());
+}
+
 }  // namespace arc
diff --git a/chrome/browser/chromeos/arc/voice_interaction/highlighter_controller_client.cc b/chrome/browser/chromeos/arc/voice_interaction/highlighter_controller_client.cc
index 89a2e44..9446c5c 100644
--- a/chrome/browser/chromeos/arc/voice_interaction/highlighter_controller_client.cc
+++ b/chrome/browser/chromeos/arc/voice_interaction/highlighter_controller_client.cc
@@ -4,11 +4,12 @@
 
 #include "chrome/browser/chromeos/arc/voice_interaction/highlighter_controller_client.h"
 
-#include "ash/highlighter/highlighter_controller.h"
-#include "ash/highlighter/highlighter_selection_observer.h"
-#include "ash/shell.h"
+#include "ash/public/interfaces/constants.mojom.h"
 #include "base/time/time.h"
 #include "chrome/browser/chromeos/arc/voice_interaction/arc_voice_interaction_framework_service.h"
+#include "content/public/common/service_manager_connection.h"
+#include "mojo/public/cpp/bindings/binding.h"
+#include "services/service_manager/public/cpp/connector.h"
 
 namespace arc {
 
@@ -18,15 +19,44 @@
 
 HighlighterControllerClient::HighlighterControllerClient(
     ArcVoiceInteractionFrameworkService* service)
-    : service_(service) {
-  ash::Shell::Get()->highlighter_controller()->SetObserver(this);
+    : binding_(this), service_(service) {}
+
+HighlighterControllerClient::~HighlighterControllerClient() = default;
+
+void HighlighterControllerClient::Attach() {
+  ConnectToHighlighterController();
+  ash::mojom::HighlighterControllerClientPtr client;
+  binding_.Bind(mojo::MakeRequest(&client));
+  highlighter_controller_->SetClient(std::move(client));
 }
 
-HighlighterControllerClient::~HighlighterControllerClient() {
-  if (ash::Shell::HasInstance() &&
-      ash::Shell::Get()->highlighter_controller()) {
-    ash::Shell::Get()->highlighter_controller()->SetObserver(nullptr);
-  }
+void HighlighterControllerClient::Detach() {
+  binding_.Close();
+}
+
+void HighlighterControllerClient::SetControllerForTesting(
+    ash::mojom::HighlighterControllerPtr controller) {
+  highlighter_controller_ = std::move(controller);
+}
+
+void HighlighterControllerClient::SimulateSelectionTimeoutForTesting() {
+  DCHECK(delay_timer_ && delay_timer_->IsRunning());
+  delay_timer_->user_task().Run();
+  delay_timer_.reset();
+}
+
+void HighlighterControllerClient::FlushMojoForTesting() {
+  highlighter_controller_.FlushForTesting();
+}
+
+void HighlighterControllerClient::ConnectToHighlighterController() {
+  // Tests may bind to their own HighlighterController.
+  if (highlighter_controller_)
+    return;
+
+  content::ServiceManagerConnection::GetForProcess()
+      ->GetConnector()
+      ->BindInterface(ash::mojom::kServiceName, &highlighter_controller_);
 }
 
 void HighlighterControllerClient::HandleSelection(const gfx::Rect& rect) {
@@ -40,8 +70,6 @@
   delay_timer_->Reset();
 }
 
-void HighlighterControllerClient::HandleFailedSelection() {}
-
 void HighlighterControllerClient::HandleEnabledStateChange(bool enabled) {
   // ArcVoiceInteractionFrameworkService::HideMetalayer() causes the container
   // to show a toast-like prompt. This toast is redundant and causes
diff --git a/chrome/browser/chromeos/arc/voice_interaction/highlighter_controller_client.h b/chrome/browser/chromeos/arc/voice_interaction/highlighter_controller_client.h
index 10243fe..cfa55e5 100644
--- a/chrome/browser/chromeos/arc/voice_interaction/highlighter_controller_client.h
+++ b/chrome/browser/chromeos/arc/voice_interaction/highlighter_controller_client.h
@@ -5,10 +5,9 @@
 #ifndef CHROME_BROWSER_CHROMEOS_ARC_VOICE_INTERACTION_HIGHLIGHTER_CONTROLLER_CLIENT_H_
 #define CHROME_BROWSER_CHROMEOS_ARC_VOICE_INTERACTION_HIGHLIGHTER_CONTROLLER_CLIENT_H_
 
-#include <memory>
-
-#include "ash/highlighter/highlighter_selection_observer.h"
+#include "ash/public/interfaces/highlighter_controller.mojom.h"
 #include "base/macros.h"
+#include "mojo/public/cpp/bindings/binding.h"
 
 namespace gfx {
 class Rect;
@@ -22,23 +21,42 @@
 
 class ArcVoiceInteractionFrameworkService;
 
-// TODO(kaznacheev) Convert observer to a mojo connection (crbug/769996)
-class HighlighterControllerClient : public ash::HighlighterSelectionObserver {
+class HighlighterControllerClient
+    : public ash::mojom::HighlighterControllerClient {
  public:
   explicit HighlighterControllerClient(
       ArcVoiceInteractionFrameworkService* service);
   ~HighlighterControllerClient() override;
 
+  // Attaches the client to the controller.
+  void Attach();
+
+  // Detaches the client from the controller.
+  void Detach();
+
+  void SetControllerForTesting(ash::mojom::HighlighterControllerPtr controller);
+
+  void SimulateSelectionTimeoutForTesting();
+
+  void FlushMojoForTesting();
+
  private:
-  // ash::HighlighterSelectionObserver:
+  void ConnectToHighlighterController();
+
+  // ash::mojom::HighlighterControllerClient:
   void HandleSelection(const gfx::Rect& rect) override;
-  void HandleFailedSelection() override;
   void HandleEnabledStateChange(bool enabled) override;
 
   void ReportSelection(const gfx::Rect& rect);
 
   bool start_session_pending() const { return delay_timer_.get(); }
 
+  // Binds to the client interface.
+  mojo::Binding<ash::mojom::HighlighterControllerClient> binding_;
+
+  // HighlighterController interface in ash.
+  ash::mojom::HighlighterControllerPtr highlighter_controller_;
+
   ArcVoiceInteractionFrameworkService* service_;
 
   std::unique_ptr<base::Timer> delay_timer_;
diff --git a/components/arc/test/fake_voice_interaction_framework_instance.cc b/components/arc/test/fake_voice_interaction_framework_instance.cc
index 37983df..a7098ea 100644
--- a/components/arc/test/fake_voice_interaction_framework_instance.cc
+++ b/components/arc/test/fake_voice_interaction_framework_instance.cc
@@ -28,10 +28,16 @@
 }
 
 void FakeVoiceInteractionFrameworkInstance::
-    StartVoiceInteractionSessionForRegion(const gfx::Rect& region) {}
+    StartVoiceInteractionSessionForRegion(const gfx::Rect& region) {
+  start_session_for_region_count_++;
+  selected_region_ = region;
+}
 
 void FakeVoiceInteractionFrameworkInstance::SetMetalayerVisibility(
-    bool visible) {}
+    bool visible) {
+  set_metalayer_visibility_count_++;
+  metalayer_visible_ = visible;
+}
 
 void FakeVoiceInteractionFrameworkInstance::SetVoiceInteractionEnabled(
     bool enable,
diff --git a/components/arc/test/fake_voice_interaction_framework_instance.h b/components/arc/test/fake_voice_interaction_framework_instance.h
index 39167bb..9dc3eaf 100644
--- a/components/arc/test/fake_voice_interaction_framework_instance.h
+++ b/components/arc/test/fake_voice_interaction_framework_instance.h
@@ -8,6 +8,7 @@
 #include <stddef.h>
 
 #include "components/arc/common/voice_interaction_framework.mojom.h"
+#include "ui/gfx/geometry/rect.h"
 
 namespace arc {
 
@@ -36,12 +37,33 @@
   size_t toggle_session_count() const { return toggle_session_count_; }
   size_t setup_wizard_count() const { return setup_wizard_count_; }
   size_t show_settings_count() const { return show_settings_count_; }
+  size_t set_metalayer_visibility_count() const {
+    return set_metalayer_visibility_count_;
+  }
+  bool metalayer_visible() const { return metalayer_visible_; }
+  size_t start_session_for_region_count() const {
+    return start_session_for_region_count_;
+  }
+  const gfx::Rect& selected_region() const { return selected_region_; }
+
+  void ResetCounters() {
+    start_session_count_ = 0u;
+    toggle_session_count_ = 0u;
+    setup_wizard_count_ = 0u;
+    show_settings_count_ = 0u;
+    set_metalayer_visibility_count_ = 0u;
+    start_session_for_region_count_ = 0u;
+  }
 
  private:
   size_t start_session_count_ = 0u;
   size_t toggle_session_count_ = 0u;
   size_t setup_wizard_count_ = 0u;
   size_t show_settings_count_ = 0u;
+  size_t set_metalayer_visibility_count_ = 0u;
+  bool metalayer_visible_ = true;
+  size_t start_session_for_region_count_ = 0u;
+  gfx::Rect selected_region_;
 
   DISALLOW_COPY_AND_ASSIGN(FakeVoiceInteractionFrameworkInstance);
 };