From b59764323fdf4b4408045662b51b9c8a6ff3151d Mon Sep 17 00:00:00 2001
From: Amanda Lin Dietz <aldietz@google.com>
Date: Wed, 18 Dec 2024 11:07:54 -0800
Subject: [PATCH] Reland "[FaceGaze] Add confirmation dialogs for
 feature/cursor/actions buttons"

This is a reland of commit c41380632ca608590fb12955865d759c8ecb3dd9

The original commit was reverted under suspicion of introducing
test failures on linux-chromeos-dbg, but the test failures
persisted after revert and were definitively traced back to a
different commit. Relanding this CL as-is.

crbug.com/384012319 filed for the test failures.

Bug: 371199351

Original change's description:
> [FaceGaze] Add confirmation dialogs for feature/cursor/actions buttons
>
> * Show standard confirmation dialog when feature is toggled off.
> * Show standard confirmation dialog when cursor control is toggled off.
> * Show standard confirmation dialog when actions are toggled off.
> * To enable user to use FaceGaze to interact with the dialog,
> add a set of sentinel prefs to indicate when users have toggled
> the button off.
> * To enable JS to force feature off without showing a dialog,
> add a sentinel pref for showing a confirmation dialog that is
> set to false when FaceGaze is forcing the feature off.
> * Update all JS to only interact with the set of sentinel prefs.
> * Add tests.
> * Leave cursor/actions strings as translateable=false for now.
>
> Design doc:
> https://docs.google.com/document/d/1ULHXI_QK6ET1Inyrm_9DrIpiecVFc3Z7kyyv6cs0oJ0/edit?usp=sharing
>
> Screenshots:
> https://screenshot.googleplex.com/3f6ir9GbJC7zd5q
> https://screenshot.googleplex.com/ra4nbokD8K4Cpub
> https://screenshot.googleplex.com/6fd2gzZvx8Gmg3W
>
> Change-Id: Iccb932657e9613cef670f2638e5dfa0794d0e3db
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6042086
> Reviewed-by: Akihiro Ota <akihiroota@chromium.org>
> Commit-Queue: Amanda Lin Dietz <aldietz@google.com>
> Reviewed-by: Xiaohui Chen <xiaohuic@chromium.org>
> Reviewed-by: James Cook <jamescook@chromium.org>
> Reviewed-by: Kyle Horimoto <khorimoto@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1395618}

Change-Id: I2a6ff4a9e251c86b66ffd36af778e06531423ce4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6101831
Reviewed-by: James Cook <jamescook@chromium.org>
Auto-Submit: Amanda Lin Dietz <aldietz@google.com>
Reviewed-by: Akihiro Ota <akihiroota@chromium.org>
Reviewed-by: Kyle Horimoto <khorimoto@chromium.org>
Commit-Queue: Amanda Lin Dietz <aldietz@google.com>
Reviewed-by: Xiaohui Chen <xiaohuic@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1398090}
---
 ash/accessibility/accessibility_controller.cc | 149 ++++++++
 ash/accessibility/accessibility_controller.h  |  24 ++
 .../accessibility_controller_unittest.cc      |   8 +
 ash/ash_strings.grd                           |  11 +
 ...ACEGAZE_DISABLE_CONFIRMATION_TEXT.png.sha1 |   1 +
 ash/constants/ash_pref_names.h                |  43 +++
 .../ash/accessibility/facegaze_browsertest.cc | 340 +++++++++++++++++-
 .../api/settings_private/prefs_util.cc        |   9 +
 .../prefs/pref_service_incognito_allowlist.cc |   4 +
 .../os_a11y_page/facegaze_actions_card.html   |   2 +-
 .../os_a11y_page/facegaze_cursor_card.html    |   2 +-
 .../os_a11y_page/facegaze_subpage.html        |   2 +-
 .../settings/os_a11y_page/facegaze_subpage.ts |   4 +-
 .../facegaze/constants.ts                     |   6 +
 .../accessibility_common/facegaze/facegaze.ts |   9 +-
 .../facegaze/web_cam_face_landmarker.ts       |   5 +-
 .../facegaze_actions_card_test.ts             |   6 +-
 .../os_a11y_page/facegaze_cursor_card_test.ts |   4 +-
 .../os_a11y_page/facegaze_subpage_test.ts     |  12 +-
 19 files changed, 623 insertions(+), 18 deletions(-)
 create mode 100644 ash/ash_strings_grd/IDS_ASH_FACEGAZE_DISABLE_CONFIRMATION_TEXT.png.sha1

diff --git a/ash/accessibility/accessibility_controller.cc b/ash/accessibility/accessibility_controller.cc
index 2cbbb49b5f02f..57010995270a7 100644
--- a/ash/accessibility/accessibility_controller.cc
+++ b/ash/accessibility/accessibility_controller.cc
@@ -1254,6 +1254,14 @@ void AccessibilityController::RegisterProfilePrefs(
   registry->RegisterBooleanPref(
       prefs::kAccessibilityTabletModeShelfNavigationButtonsEnabled, false);
   registry->RegisterBooleanPref(prefs::kAccessibilityFaceGazeEnabled, false);
+  registry->RegisterBooleanPref(prefs::kAccessibilityFaceGazeEnabledSentinel,
+                                false);
+  registry->RegisterBooleanPref(
+      prefs::kAccessibilityFaceGazeEnabledSentinelShowDialog, true);
+  registry->RegisterBooleanPref(
+      prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel, true);
+  registry->RegisterBooleanPref(
+      prefs::kAccessibilityFaceGazeActionsEnabledSentinel, true);
   registry->RegisterBooleanPref(prefs::kAccessibilityDisableTrackpadEnabled,
                                 false);
   registry->RegisterIntegerPref(prefs::kAccessibilityDisableTrackpadMode,
@@ -2574,6 +2582,8 @@ void AccessibilityController::ObservePrefs(PrefService* prefs) {
     // Features will be initialized from current prefs later.
   }
 
+  // TODO(crbug.com/383754550): Consider updating calls from
+  // base::Unretained(this) to GetWeakPtr().
   pref_change_registrar_->Add(
       prefs::kAccessibilityAutoclickDelayMs,
       base::BindRepeating(
@@ -2766,6 +2776,25 @@ void AccessibilityController::ObservePrefs(PrefService* prefs) {
 
   if (::features::IsAccessibilityFaceGazeEnabled()) {
     UpdateFaceGazeFromPrefs();
+    pref_change_registrar_->Add(
+        prefs::kAccessibilityFaceGazeEnabledSentinel,
+        base::BindRepeating(&AccessibilityController::OnFaceGazeSentinelChanged,
+                            base::Unretained(this),
+                            prefs::kAccessibilityFaceGazeEnabledSentinel,
+                            prefs::kAccessibilityFaceGazeEnabled));
+    pref_change_registrar_->Add(
+        prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel,
+        base::BindRepeating(
+            &AccessibilityController::OnFaceGazeSentinelChanged,
+            base::Unretained(this),
+            prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel,
+            prefs::kAccessibilityFaceGazeCursorControlEnabled));
+    pref_change_registrar_->Add(
+        prefs::kAccessibilityFaceGazeActionsEnabledSentinel,
+        base::BindRepeating(&AccessibilityController::OnFaceGazeSentinelChanged,
+                            base::Unretained(this),
+                            prefs::kAccessibilityFaceGazeActionsEnabledSentinel,
+                            prefs::kAccessibilityFaceGazeActionsEnabled));
   }
   if (::features::IsAccessibilityFlashScreenFeatureEnabled()) {
     UpdateFlashNotificationsFromPrefs();
@@ -3004,6 +3033,43 @@ void AccessibilityController::UpdateFaceGazeFromPrefs() {
   if (!::features::IsAccessibilityFaceGazeEnabled()) {
     return;
   }
+
+  const bool enabled =
+      active_user_prefs_->GetBoolean(prefs::kAccessibilityFaceGazeEnabled);
+  const bool sentinel_enabled = active_user_prefs_->GetBoolean(
+      prefs::kAccessibilityFaceGazeEnabledSentinel);
+  // Sentinel and behavior pref are not in sync.
+  if (enabled != sentinel_enabled) {
+    if (enabled) {
+      active_user_prefs_->SetBoolean(
+          prefs::kAccessibilityFaceGazeEnabledSentinel, true);
+    } else {
+      // Set sentinel pref to false without showing the dialog.
+      active_user_prefs_->SetBoolean(
+          prefs::kAccessibilityFaceGazeEnabledSentinelShowDialog, false);
+      active_user_prefs_->SetBoolean(
+          prefs::kAccessibilityFaceGazeEnabledSentinel, false);
+    }
+  }
+
+  const bool cursor_control_enabled = active_user_prefs_->GetBoolean(
+      prefs::kAccessibilityFaceGazeCursorControlEnabled);
+  const bool cursor_control_sentinel_enabled = active_user_prefs_->GetBoolean(
+      prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel);
+  if (cursor_control_enabled != cursor_control_sentinel_enabled) {
+    active_user_prefs_->SetBoolean(
+        prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel,
+        cursor_control_enabled);
+  }
+
+  const bool actions_enabled = active_user_prefs_->GetBoolean(
+      prefs::kAccessibilityFaceGazeActionsEnabled);
+  const bool actions_sentinel_enabled = active_user_prefs_->GetBoolean(
+      prefs::kAccessibilityFaceGazeActionsEnabledSentinel);
+  if (actions_enabled != actions_sentinel_enabled) {
+    active_user_prefs_->SetBoolean(
+        prefs::kAccessibilityFaceGazeActionsEnabledSentinel, actions_enabled);
+  }
 }
 
 void AccessibilityController::UpdateFlashNotificationsFromPrefs() {
@@ -3626,6 +3692,28 @@ gfx::Rect AccessibilityController::GetConfirmationDialogBoundsInScreen() {
   return confirmation_dialog_.get()->GetWidget()->GetWindowBoundsInScreen();
 }
 
+void AccessibilityController::ShowFeatureDisableDialog(
+    int window_title_text_id,
+    base::OnceClosure on_accept_callback,
+    base::OnceClosure on_cancel_callback) {
+  if (disable_dialog_) {
+    // If a dialog is already being shown we do not show a new one.
+    // Instead, run the on_close_callback on the new dialog to indicate
+    // it was closed without the user taking any action.
+    // This is consistent with AcceleratorController.
+    std::move(on_cancel_callback).Run();
+    return;
+  }
+
+  auto* dialog = new AccessibilityFeatureDisableDialog(
+      window_title_text_id, std::move(on_accept_callback),
+      std::move(on_cancel_callback));
+  disable_dialog_ = dialog->GetWeakPtr();
+  if (show_disable_dialog_callback_for_testing_) {
+    show_disable_dialog_callback_for_testing_.Run();
+  }
+}
+
 void AccessibilityController::PreviewFlashNotification() const {
   flash_screen_controller_->PreviewFlash();
 }
@@ -3938,6 +4026,11 @@ void AccessibilityController::AddShowConfirmationDialogCallbackForTesting(
   show_confirmation_dialog_callback_for_testing_ = std::move(callback);
 }
 
+void AccessibilityController::AddFeatureDisableDialogCallbackForTesting(
+    base::RepeatingCallback<void()> callback) {
+  show_disable_dialog_callback_for_testing_ = std::move(callback);
+}
+
 bool AccessibilityController::VerifyFeaturesDataForTesting() {
   return VerifyFeaturesData();
 }
@@ -3981,6 +4074,62 @@ void AccessibilityController::ScrollAtPoint(
   std::ignore = host->GetEventSink()->OnEventFromSource(&wheel);
 }
 
+void AccessibilityController::OnFaceGazeSentinelChanged(
+    const std::string& sentinel_pref,
+    const std::string& behavior_pref) {
+  DCHECK(active_user_prefs_);
+  if (active_user_prefs_->GetBoolean(sentinel_pref)) {
+    active_user_prefs_->SetBoolean(behavior_pref, true);
+    return;
+  }
+
+  // Set to FaceGaze disable confirmation text by default to ensure a valid
+  // value.
+  int window_title_text_id = IDS_ASH_FACEGAZE_DISABLE_CONFIRMATION_TEXT;
+  if (sentinel_pref == prefs::kAccessibilityFaceGazeEnabledSentinel) {
+    const bool show_dialog = active_user_prefs_->GetBoolean(
+        prefs::kAccessibilityFaceGazeEnabledSentinelShowDialog);
+    if (!show_dialog) {
+      face_gaze().SetEnabled(false);
+      // Reset show dialog pref to default after pref is handled.
+      active_user_prefs_->SetBoolean(
+          prefs::kAccessibilityFaceGazeEnabledSentinelShowDialog, true);
+      return;
+    }
+  } else if (sentinel_pref ==
+             prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel) {
+    window_title_text_id =
+        IDS_ASH_FACEGAZE_CURSOR_CONTROL_DISABLE_CONFIRMATION_TEXT;
+  } else if (sentinel_pref ==
+             prefs::kAccessibilityFaceGazeActionsEnabledSentinel) {
+    window_title_text_id = IDS_ASH_FACEGAZE_ACTIONS_DISABLE_CONFIRMATION_TEXT;
+  }
+
+  ShowFeatureDisableDialog(
+      window_title_text_id,
+      BindOnce(&AccessibilityController::OnFaceGazeDisableDialogClosed,
+               GetWeakPtr(), sentinel_pref, behavior_pref,
+               /*dialog_accepted=*/true),
+      BindOnce(&AccessibilityController::OnFaceGazeDisableDialogClosed,
+               GetWeakPtr(), sentinel_pref, behavior_pref,
+               /*dialog_accepted=*/false));
+}
+
+void AccessibilityController::OnFaceGazeDisableDialogClosed(
+    const std::string& sentinel_pref,
+    const std::string& behavior_pref,
+    bool dialog_accepted) {
+  if (dialog_accepted) {
+    // After confirmation, set behavior pref to false to turn off the feature.
+    active_user_prefs_->SetBoolean(behavior_pref, false);
+  } else {
+    // Ensure sentinel pref is in sync with behavior pref if dialog is
+    // cancelled.
+    active_user_prefs_->SetBoolean(sentinel_pref, true);
+  }
+  disable_dialog_.reset();
+}
+
 void AccessibilityController::UpdateFaceGazeBubble(const std::u16string& text,
                                                    bool is_warning) {
   if (!facegaze_bubble_controller_ ||
diff --git a/ash/accessibility/accessibility_controller.h b/ash/accessibility/accessibility_controller.h
index 505d6a795e670..10792e2d7f6be 100644
--- a/ash/accessibility/accessibility_controller.h
+++ b/ash/accessibility/accessibility_controller.h
@@ -55,6 +55,7 @@ namespace ash {
 class AccessibilityConfirmationDialog;
 class AccessibilityControllerClient;
 class AccessibilityEventRewriter;
+class AccessibilityFeatureDisableDialog;
 class AccessibilityHighlightController;
 class AccessibilityObserver;
 enum class AccessibilityPanelState;
@@ -644,6 +645,12 @@ class ASH_EXPORT AccessibilityController
       std::optional<int> timeout_seconds = std::nullopt);
   gfx::Rect GetConfirmationDialogBoundsInScreen();
 
+  // Shows a dialog to disable a feature with the given text, and calls the
+  // relevant callback when the dialog is accepted or cancelled.
+  void ShowFeatureDisableDialog(int window_title_text_id,
+                                base::OnceClosure on_accept_callback,
+                                base::OnceClosure on_cancel_callback);
+
   void PreviewFlashNotification() const;
 
   // SessionObserver:
@@ -679,6 +686,9 @@ class ASH_EXPORT AccessibilityController
   AccessibilityConfirmationDialog* GetConfirmationDialogForTest() {
     return confirmation_dialog_.get();
   }
+  AccessibilityFeatureDisableDialog* GetFeatureDisableDialogForTest() {
+    return disable_dialog_.get();
+  }
 
   bool enable_chromevox_volume_slide_gesture() {
     return enable_chromevox_volume_slide_gesture_;
@@ -718,6 +728,9 @@ class ASH_EXPORT AccessibilityController
   void AddShowConfirmationDialogCallbackForTesting(
       base::RepeatingCallback<void()> callback);
 
+  void AddFeatureDisableDialogCallbackForTesting(
+      base::RepeatingCallback<void()> callback);
+
   bool VerifyFeaturesDataForTesting();
 
   SelectToSpeakEventHandler* GetSelectToSpeakEventHandlerForTesting() const {
@@ -813,6 +826,12 @@ class ASH_EXPORT AccessibilityController
   void OnDisableTouchpadDialogDismissed();
   void ExternalDeviceConnected();
 
+  void OnFaceGazeSentinelChanged(const std::string& sentinel_pref,
+                                 const std::string& behavior_pref);
+  void OnFaceGazeDisableDialogClosed(const std::string& sentinel_pref,
+                                     const std::string& behavior_pref,
+                                     bool dialog_accepted);
+
   void RecordSelectToSpeakSpeechDuration(SelectToSpeakState old_state,
                                          SelectToSpeakState new_state);
 
@@ -912,6 +931,11 @@ class ASH_EXPORT AccessibilityController
   base::RepeatingCallback<void()>
       show_confirmation_dialog_callback_for_testing_;
 
+  // The current AccessibilityFeatureDisableDialog, if one exists.
+  base::WeakPtr<AccessibilityFeatureDisableDialog> disable_dialog_;
+
+  base::RepeatingCallback<void()> show_disable_dialog_callback_for_testing_;
+
   base::Time select_to_speak_speech_start_time_;
 
   base::ScopedObservation<InputDeviceSettingsController,
diff --git a/ash/accessibility/accessibility_controller_unittest.cc b/ash/accessibility/accessibility_controller_unittest.cc
index 7e73e8e5042ef..394880ae0a127 100644
--- a/ash/accessibility/accessibility_controller_unittest.cc
+++ b/ash/accessibility/accessibility_controller_unittest.cc
@@ -342,6 +342,14 @@ TEST_F(AccessibilityControllerTest, PrefsAreRegistered) {
       prefs()->FindPreference(prefs::kAccessibilityFaceGazePrecisionClick));
   EXPECT_TRUE(prefs()->FindPreference(
       prefs::kAccessibilityFaceGazePrecisionClickSpeedFactor));
+  EXPECT_TRUE(
+      prefs()->FindPreference(prefs::kAccessibilityFaceGazeEnabledSentinel));
+  EXPECT_TRUE(prefs()->FindPreference(
+      prefs::kAccessibilityFaceGazeEnabledSentinelShowDialog));
+  EXPECT_TRUE(prefs()->FindPreference(
+      prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel));
+  EXPECT_TRUE(prefs()->FindPreference(
+      prefs::kAccessibilityFaceGazeActionsEnabledSentinel));
   EXPECT_TRUE(prefs()->FindPreference(prefs::kAccessibilityCaretBlinkInterval));
   EXPECT_TRUE(
       prefs()->FindPreference(prefs::kAccessibilityFlashNotificationsEnabled));
diff --git a/ash/ash_strings.grd b/ash/ash_strings.grd
index ce68e337fccea..54d01626c61b6 100644
--- a/ash/ash_strings.grd
+++ b/ash/ash_strings.grd
@@ -1512,6 +1512,17 @@ Style notes:
 You can also use the keyboard shortcut. First, highlight text, then press <ph name="modifier">$1<ex>launcher</ex></ph> + s.
       </message>
 
+      <!-- FaceGaze dialog strings -->
+      <message name="IDS_ASH_FACEGAZE_DISABLE_CONFIRMATION_TEXT" desc="The text for the modal dialog shown when the user disables FaceGaze, to confirm they meant to disable the feature.">
+        Are you sure you want to turn off face control?
+      </message>
+      <message name="IDS_ASH_FACEGAZE_CURSOR_CONTROL_DISABLE_CONFIRMATION_TEXT" desc="The text for the modal dialog shown when the user disables cursor control in FaceGaze, to confirm they meant to disable the feature." translateable="false">
+        Are you sure you want to turn off mouse cursor control?
+      </message>
+      <message name="IDS_ASH_FACEGAZE_ACTIONS_DISABLE_CONFIRMATION_TEXT" desc="The text for the modal dialog shown when the user disables performing actions using facial gestures in FaceGaze, to confirm they meant to disable the feature." translateable="false">
+        Are you sure you want to turn off facial gestures?
+      </message>
+
       <!-- Accessibility nudges -->
       <message name="IDS_ASH_ACCESSIBILITY_NUDGE_DICTATION_NO_FOCUSED_TEXT_FIELD" desc="Displayed in a nudge when Dictation is stopped automatically because there is no focused text field.">
         Go to a text field to use Dictation
diff --git a/ash/ash_strings_grd/IDS_ASH_FACEGAZE_DISABLE_CONFIRMATION_TEXT.png.sha1 b/ash/ash_strings_grd/IDS_ASH_FACEGAZE_DISABLE_CONFIRMATION_TEXT.png.sha1
new file mode 100644
index 0000000000000..d114f7b71b74e
--- /dev/null
+++ b/ash/ash_strings_grd/IDS_ASH_FACEGAZE_DISABLE_CONFIRMATION_TEXT.png.sha1
@@ -0,0 +1 @@
+2ef8fbb30486b5eb3b1bee216347beaa3de2040c
\ No newline at end of file
diff --git a/ash/constants/ash_pref_names.h b/ash/constants/ash_pref_names.h
index a53459a18384b..01642d62b8e65 100644
--- a/ash/constants/ash_pref_names.h
+++ b/ash/constants/ash_pref_names.h
@@ -917,6 +917,49 @@ inline constexpr char kAccessibilityFaceGazePrecisionClick[] =
 // should be dampened by during a precision click.
 inline constexpr char kAccessibilityFaceGazePrecisionClickSpeedFactor[] =
     "settings.a11y.face_gaze.precision_click_speed_factor";
+// A boolean pref which indicates when a request has been made to change the
+// FaceGaze enabled state. This pref acts a sentinel for the requested state.
+// The feature uses this pref to determine whether FaceGaze should be 1) enabled
+// 2) disabled, or 3) a dialog needs to be shown to confirm whether the user
+// wants to disable the feature. Using a separate sentinel pref to store the
+// requested state here ensures the kAccessibilityFaceGazeEnabled pref always
+// accurately reflects the feature state, specifically in the case where the
+// sentinel pref is set to false and the behavior pref must remain true until
+// the confirmation dialog is accepted or cancelled. This is to ensure the user
+// can interact with the dialog with FaceGaze as expected. In all other
+// scenarios, the behavior pref and sentinel pref are kept in sync.
+inline constexpr char kAccessibilityFaceGazeEnabledSentinel[] =
+    "settings.a11y.face_gaze.enabled_sentinel";
+// A boolean pref which indicates whether the confirmation dialog should be
+// shown when kAccessibilityFaceGazeEnabledSentinel is set to false.
+inline constexpr char kAccessibilityFaceGazeEnabledSentinelShowDialog[] =
+    "settings.a11y.face_gaze.enabled_sentinel_show_dialog";
+// A boolean pref which indicates the requested enabled state for FaceGaze
+// cursor control. This pref acts as a sentinel for the requested cursor control
+// state. The feature uses this pref to determine whether cursor control should
+// be 1) enabled or 2) a dialog needs to be shown to confirm whether the user
+// wants to disable the feature. Using a separate sentinel pref to store the
+// requested state here ensures the kAccessibilityFaceGazeCursorControlEnabled
+// pref always accurately reflects the feature state, specifically in the case
+// where the sentinel pref is set to false and the behavior pref must remain
+// true until the confirmation dialog is accepted or cancelled. This is to
+// ensure the user can interact with the dialog with FaceGaze as expected. In
+// all other scenarios, the behavior pref and sentinel pref are kept in sync.
+inline constexpr char kAccessibilityFaceGazeCursorControlEnabledSentinel[] =
+    "settings.a11y.face_gaze.cursor_control_enabled_sentinel";
+// A boolean pref which indicates the requested enabled state for FaceGaze
+// actions. This pref acts as a sentinel for the requested actions
+// state. The feature uses this pref to determine whether actions should
+// be 1) enabled or 2) a dialog needs to be shown to confirm whether the user
+// wants to disable the feature. Using a separate sentinel pref to store the
+// requested state here ensures the kAccessibilityFaceGazeActionsEnabled pref
+// always accurately reflects the feature state, specifically in the case where
+// the sentinel pref is set to false and the behavior pref must remain true
+// until the confirmation dialog is accepted or cancelled. This is to ensure the
+// user can interact with the dialog with FaceGaze as expected. In all other
+// scenarios, the behavior pref and sentinel pref are kept in sync.
+inline constexpr char kAccessibilityFaceGazeActionsEnabledSentinel[] =
+    "settings.a11y.face_gaze.actions_enabled_sentinel";
 
 // A boolean pref which determines whether the accessibility menu shows
 // regardless of the state of a11y features.
diff --git a/chrome/browser/ash/accessibility/facegaze_browsertest.cc b/chrome/browser/ash/accessibility/facegaze_browsertest.cc
index 31371d359b70b..57355e68771d4 100644
--- a/chrome/browser/ash/accessibility/facegaze_browsertest.cc
+++ b/chrome/browser/ash/accessibility/facegaze_browsertest.cc
@@ -9,6 +9,7 @@
 #include "ash/accessibility/ui/accessibility_confirmation_dialog.h"
 #include "ash/constants/ash_pref_names.h"
 #include "ash/shell.h"
+#include "ash/system/accessibility/accessibility_feature_disable_dialog.h"
 #include "base/run_loop.h"
 #include "base/test/bind.h"
 #include "base/test/scoped_feature_list.h"
@@ -683,7 +684,7 @@ IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, DefaultBehavior) {
       GetPrefs()->GetDict(prefs::kAccessibilityFaceGazeGesturesToConfidence);
   ASSERT_EQ(gestures_to_macros.size(), 2u);
   ASSERT_EQ(gestures_to_confidences.size(), 2u);
-  ASSERT_EQ(/* MOUTH_SMILE */ 35,
+  ASSERT_EQ(/* MOUSE_CLICK_LEFT */ 35,
             gestures_to_macros.FindInt(
                 FaceGazeTestUtils::ToString(FaceGazeGesture::MOUTH_SMILE)));
   ASSERT_EQ(/* SCROLL */ 50,
@@ -695,4 +696,341 @@ IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, DefaultBehavior) {
                     FaceGazeTestUtils::ToString(FaceGazeGesture::JAW_OPEN)));
 }
 
+IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, EnableNoDialog) {
+  auto* controller = ash::Shell::Get()->accessibility_controller();
+  auto* prefs = GetPrefs();
+
+  base::RunLoop dialog_waiter;
+  controller->AddFeatureDisableDialogCallbackForTesting(
+      base::BindLambdaForTesting([&dialog_waiter]() { dialog_waiter.Quit(); }));
+
+  // Setting sentinel value to true should enable the feature without showing
+  // the feature disable dialog.
+  prefs->SetBoolean(prefs::kAccessibilityFaceGazeEnabledSentinel, true);
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
+  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());
+}
+
+IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, DisableDialogAccept) {
+  auto* controller = ash::Shell::Get()->accessibility_controller();
+  auto* prefs = GetPrefs();
+
+  base::RunLoop dialog_waiter;
+  controller->AddFeatureDisableDialogCallbackForTesting(
+      base::BindLambdaForTesting([&dialog_waiter]() { dialog_waiter.Quit(); }));
+
+  // Enabling FaceGaze should not show the feature disable dialog.
+  utils()->EnableFaceGaze(Config().Default());
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabledSentinel));
+  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());
+
+  // Setting sentinel value to false should show the feature disable dialog and
+  // leave the behavior pref unchanged.
+  prefs->SetBoolean(prefs::kAccessibilityFaceGazeEnabledSentinel, false);
+  dialog_waiter.Run();
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
+  ASSERT_FALSE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabledSentinel));
+  ASSERT_NE(nullptr, controller->GetFeatureDisableDialogForTest());
+
+  base::RunLoop pref_waiter;
+  PrefChangeRegistrar change_observer;
+  change_observer.Init(prefs);
+  change_observer.Add(prefs::kAccessibilityFaceGazeEnabled,
+                      pref_waiter.QuitClosure());
+
+  // Accepting the dialog should turn off FaceGaze.
+  controller->GetFeatureDisableDialogForTest()->Accept();
+  pref_waiter.Run();
+
+  // Assert behavior and sentinel prefs are in sync.
+  ASSERT_FALSE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
+  ASSERT_FALSE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabledSentinel));
+}
+
+IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, DisableDialogNoShow) {
+  auto* controller = ash::Shell::Get()->accessibility_controller();
+  auto* prefs = GetPrefs();
+
+  // Enabling FaceGaze should not show the feature disable dialog.
+  utils()->EnableFaceGaze(Config().Default());
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabledSentinel));
+  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());
+
+  base::RunLoop pref_waiter;
+  PrefChangeRegistrar change_observer;
+  change_observer.Init(prefs);
+  change_observer.Add(prefs::kAccessibilityFaceGazeEnabled,
+                      pref_waiter.QuitClosure());
+
+  // Setting show dialog value to false should allow the feature to be set to
+  // false when the sentinel is set to false.
+  prefs->SetBoolean(prefs::kAccessibilityFaceGazeEnabledSentinelShowDialog,
+                    false);
+  prefs->SetBoolean(prefs::kAccessibilityFaceGazeEnabledSentinel, false);
+  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());
+
+  pref_waiter.Run();
+
+  // Assert behavior and sentinel prefs are in sync.
+  ASSERT_FALSE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
+  ASSERT_FALSE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabledSentinel));
+}
+
+IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, DisableDialogCancel) {
+  auto* controller = ash::Shell::Get()->accessibility_controller();
+  auto* prefs = GetPrefs();
+
+  base::RunLoop dialog_waiter;
+  controller->AddFeatureDisableDialogCallbackForTesting(
+      base::BindLambdaForTesting([&dialog_waiter]() { dialog_waiter.Quit(); }));
+
+  // Enabling FaceGaze should not show the feature disable dialog.
+  utils()->EnableFaceGaze(Config().Default());
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabledSentinel));
+  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());
+
+  // Setting sentinel value to false should show the feature disable dialog and
+  // leave the behavior pref unchanged.
+  prefs->SetBoolean(prefs::kAccessibilityFaceGazeEnabledSentinel, false);
+  dialog_waiter.Run();
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
+  ASSERT_NE(nullptr, controller->GetFeatureDisableDialogForTest());
+
+  base::RunLoop pref_waiter;
+  PrefChangeRegistrar change_observer;
+  change_observer.Init(prefs);
+  change_observer.Add(prefs::kAccessibilityFaceGazeEnabledSentinel,
+                      pref_waiter.QuitClosure());
+
+  // Cancelling the dialog should leave FaceGaze on and set the sentinel to
+  // true.
+  controller->GetFeatureDisableDialogForTest()->Cancel();
+  pref_waiter.Run();
+
+  // Assert behavior and sentinel prefs are in sync.
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabledSentinel));
+}
+
+// TODO(crbug.com/383757982): Add test API for .WithCursorControlEnabled() and
+// .WithActionsEnabled() and update tests accordingly.
+IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, EnableCursorControlNoDialog) {
+  auto* controller = ash::Shell::Get()->accessibility_controller();
+  auto* prefs = GetPrefs();
+
+  base::RunLoop dialog_waiter;
+  controller->AddFeatureDisableDialogCallbackForTesting(
+      base::BindLambdaForTesting([&dialog_waiter]() { dialog_waiter.Quit(); }));
+
+  // Enabling FaceGaze should not show the feature disable dialog.
+  utils()->EnableFaceGaze(Config().Default());
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabledSentinel));
+  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());
+
+  // Setting sentinel value to true should not show the feature disable dialog.
+  prefs->SetBoolean(prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel,
+                    true);
+  ASSERT_TRUE(
+      prefs->GetBoolean(prefs::kAccessibilityFaceGazeCursorControlEnabled));
+  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());
+}
+
+IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest,
+                       DisableCursorControlDialogAccept) {
+  auto* controller = ash::Shell::Get()->accessibility_controller();
+  auto* prefs = GetPrefs();
+
+  base::RunLoop dialog_waiter;
+  controller->AddFeatureDisableDialogCallbackForTesting(
+      base::BindLambdaForTesting([&dialog_waiter]() { dialog_waiter.Quit(); }));
+
+  // Enabling FaceGaze should not show the feature disable dialog.
+  utils()->EnableFaceGaze(Config().Default());
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabledSentinel));
+  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());
+  prefs->SetBoolean(prefs::kAccessibilityFaceGazeCursorControlEnabled, true);
+  ASSERT_TRUE(prefs->GetBoolean(
+      prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel));
+
+  // Setting sentinel value to false should show the feature disable dialog and
+  // leave the behavior pref unchanged.
+  prefs->SetBoolean(prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel,
+                    false);
+  dialog_waiter.Run();
+  ASSERT_TRUE(
+      prefs->GetBoolean(prefs::kAccessibilityFaceGazeCursorControlEnabled));
+  ASSERT_FALSE(prefs->GetBoolean(
+      prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel));
+  ASSERT_NE(nullptr, controller->GetFeatureDisableDialogForTest());
+
+  base::RunLoop pref_waiter;
+  PrefChangeRegistrar change_observer;
+  change_observer.Init(prefs);
+  change_observer.Add(prefs::kAccessibilityFaceGazeCursorControlEnabled,
+                      pref_waiter.QuitClosure());
+
+  // Accepting the dialog should turn off FaceGaze cursor control.
+  controller->GetFeatureDisableDialogForTest()->Accept();
+  pref_waiter.Run();
+
+  // Assert behavior and sentinel prefs are in sync.
+  ASSERT_FALSE(
+      prefs->GetBoolean(prefs::kAccessibilityFaceGazeCursorControlEnabled));
+  ASSERT_FALSE(prefs->GetBoolean(
+      prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel));
+}
+
+IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest,
+                       DisableCursorControlDialogCancel) {
+  auto* controller = ash::Shell::Get()->accessibility_controller();
+  auto* prefs = GetPrefs();
+
+  base::RunLoop dialog_waiter;
+  controller->AddFeatureDisableDialogCallbackForTesting(
+      base::BindLambdaForTesting([&dialog_waiter]() { dialog_waiter.Quit(); }));
+
+  // Enabling FaceGaze should not show the feature disable dialog.
+  utils()->EnableFaceGaze(Config().Default());
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabledSentinel));
+  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());
+  prefs->SetBoolean(prefs::kAccessibilityFaceGazeCursorControlEnabled, true);
+  ASSERT_TRUE(prefs->GetBoolean(
+      prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel));
+
+  // Setting sentinel value to false should show the feature disable dialog and
+  // leave the behavior pref unchanged.
+  prefs->SetBoolean(prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel,
+                    false);
+  dialog_waiter.Run();
+  ASSERT_TRUE(
+      prefs->GetBoolean(prefs::kAccessibilityFaceGazeCursorControlEnabled));
+  ASSERT_NE(nullptr, controller->GetFeatureDisableDialogForTest());
+
+  base::RunLoop pref_waiter;
+  PrefChangeRegistrar change_observer;
+  change_observer.Init(prefs);
+  change_observer.Add(prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel,
+                      pref_waiter.QuitClosure());
+
+  // Cancelling the dialog should leave FaceGaze cursor control on and set the
+  // sentinel to true.
+  controller->GetFeatureDisableDialogForTest()->Cancel();
+  pref_waiter.Run();
+
+  // Assert behavior and sentinel prefs are in sync.
+  ASSERT_TRUE(
+      prefs->GetBoolean(prefs::kAccessibilityFaceGazeCursorControlEnabled));
+  ASSERT_TRUE(prefs->GetBoolean(
+      prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel));
+}
+
+IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, EnableActionsNoDialog) {
+  auto* controller = ash::Shell::Get()->accessibility_controller();
+  auto* prefs = GetPrefs();
+
+  base::RunLoop dialog_waiter;
+  controller->AddFeatureDisableDialogCallbackForTesting(
+      base::BindLambdaForTesting([&dialog_waiter]() { dialog_waiter.Quit(); }));
+
+  // Enabling FaceGaze should not show the feature disable dialog.
+  utils()->EnableFaceGaze(Config().Default());
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabledSentinel));
+  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());
+
+  // Setting sentinel value to true should not show the feature disable dialog.
+  prefs->SetBoolean(prefs::kAccessibilityFaceGazeActionsEnabledSentinel, true);
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeActionsEnabled));
+  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());
+}
+
+IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, DisableActionsDialogAccept) {
+  auto* controller = ash::Shell::Get()->accessibility_controller();
+  auto* prefs = GetPrefs();
+
+  base::RunLoop dialog_waiter;
+  controller->AddFeatureDisableDialogCallbackForTesting(
+      base::BindLambdaForTesting([&dialog_waiter]() { dialog_waiter.Quit(); }));
+
+  // Setting sentinel value to true should not show the feature disable dialog.
+  utils()->EnableFaceGaze(Config().Default());
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabledSentinel));
+  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());
+  prefs->SetBoolean(prefs::kAccessibilityFaceGazeActionsEnabled, true);
+  ASSERT_TRUE(
+      prefs->GetBoolean(prefs::kAccessibilityFaceGazeActionsEnabledSentinel));
+
+  // Setting sentinel value to false should show the feature disable dialog and
+  // leave the behavior pref unchanged.
+  prefs->SetBoolean(prefs::kAccessibilityFaceGazeActionsEnabledSentinel, false);
+  dialog_waiter.Run();
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeActionsEnabled));
+  ASSERT_FALSE(
+      prefs->GetBoolean(prefs::kAccessibilityFaceGazeActionsEnabledSentinel));
+  ASSERT_NE(nullptr, controller->GetFeatureDisableDialogForTest());
+
+  base::RunLoop pref_waiter;
+  PrefChangeRegistrar change_observer;
+  change_observer.Init(prefs);
+  change_observer.Add(prefs::kAccessibilityFaceGazeActionsEnabled,
+                      pref_waiter.QuitClosure());
+
+  // Accepting the dialog should turn off FaceGaze actions.
+  controller->GetFeatureDisableDialogForTest()->Accept();
+  pref_waiter.Run();
+
+  // Assert behavior and sentinel prefs are in sync.
+  ASSERT_FALSE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeActionsEnabled));
+  ASSERT_FALSE(
+      prefs->GetBoolean(prefs::kAccessibilityFaceGazeActionsEnabledSentinel));
+}
+
+IN_PROC_BROWSER_TEST_F(FaceGazeIntegrationTest, DisableActionsDialogCancel) {
+  auto* controller = ash::Shell::Get()->accessibility_controller();
+  auto* prefs = GetPrefs();
+
+  base::RunLoop dialog_waiter;
+  controller->AddFeatureDisableDialogCallbackForTesting(
+      base::BindLambdaForTesting([&dialog_waiter]() { dialog_waiter.Quit(); }));
+
+  // Setting sentinel value to true should not show the feature disable dialog.
+  utils()->EnableFaceGaze(Config().Default());
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabled));
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeEnabledSentinel));
+  ASSERT_EQ(nullptr, controller->GetFeatureDisableDialogForTest());
+  prefs->SetBoolean(prefs::kAccessibilityFaceGazeActionsEnabled, true);
+  ASSERT_TRUE(
+      prefs->GetBoolean(prefs::kAccessibilityFaceGazeActionsEnabledSentinel));
+
+  // Setting sentinel value to false should show the feature disable dialog and
+  // leave the behavior pref unchanged.
+  prefs->SetBoolean(prefs::kAccessibilityFaceGazeActionsEnabledSentinel, false);
+  dialog_waiter.Run();
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeActionsEnabled));
+  ASSERT_NE(nullptr, controller->GetFeatureDisableDialogForTest());
+
+  base::RunLoop pref_waiter;
+  PrefChangeRegistrar change_observer;
+  change_observer.Init(prefs);
+  change_observer.Add(prefs::kAccessibilityFaceGazeActionsEnabledSentinel,
+                      pref_waiter.QuitClosure());
+
+  // Cancelling the dialog should leave FaceGaze actions on and set the sentinel
+  // to true.
+  controller->GetFeatureDisableDialogForTest()->Cancel();
+  pref_waiter.Run();
+
+  // Assert behavior and sentinel prefs are in sync.
+  ASSERT_TRUE(prefs->GetBoolean(prefs::kAccessibilityFaceGazeActionsEnabled));
+  ASSERT_TRUE(
+      prefs->GetBoolean(prefs::kAccessibilityFaceGazeActionsEnabledSentinel));
+}
+
 }  // namespace ash
diff --git a/chrome/browser/extensions/api/settings_private/prefs_util.cc b/chrome/browser/extensions/api/settings_private/prefs_util.cc
index 0a0679570a686..ef05604425bfd 100644
--- a/chrome/browser/extensions/api/settings_private/prefs_util.cc
+++ b/chrome/browser/extensions/api/settings_private/prefs_util.cc
@@ -779,6 +779,15 @@ const PrefsUtil::TypedPrefMap& PrefsUtil::GetAllowlistedKeys() {
       settings_api::PrefType::kBoolean;
   (*s_allowlist)[ash::prefs::kAccessibilityFaceGazePrecisionClickSpeedFactor] =
       settings_api::PrefType::kNumber;
+  (*s_allowlist)[ash::prefs::kAccessibilityFaceGazeEnabledSentinel] =
+      settings_api::PrefType::kBoolean;
+  (*s_allowlist)[ash::prefs::kAccessibilityFaceGazeEnabledSentinelShowDialog] =
+      settings_api::PrefType::kBoolean;
+  (*s_allowlist)
+      [ash::prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel] =
+          settings_api::PrefType::kBoolean;
+  (*s_allowlist)[ash::prefs::kAccessibilityFaceGazeActionsEnabledSentinel] =
+      settings_api::PrefType::kBoolean;
   (*s_allowlist)[ash::prefs::kAccessibilityCaretBlinkInterval] =
       settings_api::PrefType::kNumber;
   (*s_allowlist)[ash::prefs::kAccessibilityDisableTrackpadEnabled] =
diff --git a/chrome/browser/prefs/pref_service_incognito_allowlist.cc b/chrome/browser/prefs/pref_service_incognito_allowlist.cc
index 8da23e3f62f22..53cc913c147bb 100644
--- a/chrome/browser/prefs/pref_service_incognito_allowlist.cc
+++ b/chrome/browser/prefs/pref_service_incognito_allowlist.cc
@@ -90,6 +90,10 @@ const char* const kPersistentPrefNames[] = {
     ash::prefs::kAccessibilityFaceGazeVelocityThreshold,
     ash::prefs::kAccessibilityFaceGazePrecisionClick,
     ash::prefs::kAccessibilityFaceGazePrecisionClickSpeedFactor,
+    ash::prefs::kAccessibilityFaceGazeEnabledSentinel,
+    ash::prefs::kAccessibilityFaceGazeEnabledSentinelShowDialog,
+    ash::prefs::kAccessibilityFaceGazeCursorControlEnabledSentinel,
+    ash::prefs::kAccessibilityFaceGazeActionsEnabledSentinel,
     ash::prefs::kAccessibilityFlashNotificationsEnabled,
     ash::prefs::kAccessibilityFlashNotificationsColor,
     ash::prefs::kAccessibilityHighContrastEnabled,
diff --git a/chrome/browser/resources/ash/settings/os_a11y_page/facegaze_actions_card.html b/chrome/browser/resources/ash/settings/os_a11y_page/facegaze_actions_card.html
index 39216ae202d52..ef26dff356e26 100644
--- a/chrome/browser/resources/ash/settings/os_a11y_page/facegaze_actions_card.html
+++ b/chrome/browser/resources/ash/settings/os_a11y_page/facegaze_actions_card.html
@@ -44,7 +44,7 @@
   <settings-toggle-button
       id="faceGazeActionsEnabledButton"
       label="$i18n{faceGazeActionsEnabledLabel}"
-      pref="{{prefs.settings.a11y.face_gaze.actions_enabled}}"
+      pref="{{prefs.settings.a11y.face_gaze.actions_enabled_sentinel}}"
       disabled="[[disabled]]">
   </settings-toggle-button>
   <dom-repeat id="faceGazeActionsCommandPairs"
diff --git a/chrome/browser/resources/ash/settings/os_a11y_page/facegaze_cursor_card.html b/chrome/browser/resources/ash/settings/os_a11y_page/facegaze_cursor_card.html
index 2a82651d0f08b..142fcfbcf2a71 100644
--- a/chrome/browser/resources/ash/settings/os_a11y_page/facegaze_cursor_card.html
+++ b/chrome/browser/resources/ash/settings/os_a11y_page/facegaze_cursor_card.html
@@ -35,7 +35,7 @@
   <settings-toggle-button
       id="faceGazeCursorControlEnabledButton"
       label="$i18n{faceGazeCursorControlEnabledLabel}"
-      pref="{{prefs.settings.a11y.face_gaze.cursor_control_enabled}}">
+      pref="{{prefs.settings.a11y.face_gaze.cursor_control_enabled_sentinel}}">
   </settings-toggle-button>
   <div class="settings-box short">
     $i18n{faceGazeCursorSpeedSectionName}
diff --git a/chrome/browser/resources/ash/settings/os_a11y_page/facegaze_subpage.html b/chrome/browser/resources/ash/settings/os_a11y_page/facegaze_subpage.html
index 5d6e96a1a116c..4cc835593a11b 100644
--- a/chrome/browser/resources/ash/settings/os_a11y_page/facegaze_subpage.html
+++ b/chrome/browser/resources/ash/settings/os_a11y_page/facegaze_subpage.html
@@ -19,7 +19,7 @@
     <settings-toggle-button
         id="faceGazeToggle"
         label="[[toggleLabel_]]"
-        pref="{{prefs.settings.a11y.face_gaze.enabled}}"
+        pref="{{prefs.settings.a11y.face_gaze.enabled_sentinel}}"
         deep-link-focus-id$="[[Setting.kFaceGaze]]">
     </settings-toggle-button>
   </settings-card>
diff --git a/chrome/browser/resources/ash/settings/os_a11y_page/facegaze_subpage.ts b/chrome/browser/resources/ash/settings/os_a11y_page/facegaze_subpage.ts
index 0044286e483f8..fb8c7ef6ff3a3 100644
--- a/chrome/browser/resources/ash/settings/os_a11y_page/facegaze_subpage.ts
+++ b/chrome/browser/resources/ash/settings/os_a11y_page/facegaze_subpage.ts
@@ -46,7 +46,7 @@ export class SettingsFaceGazeSubpageElement extends
       toggleLabel_: {
         type: String,
         computed:
-            'getToggleLabel_(prefs.settings.a11y.face_gaze.enabled.value)',
+            'getToggleLabel_(prefs.settings.a11y.face_gaze.enabled_sentinel.value)',
       },
 
       supportedSettingIds: {
@@ -59,7 +59,7 @@ export class SettingsFaceGazeSubpageElement extends
   }
 
   private getToggleLabel_(): string {
-    return this.getPref('settings.a11y.face_gaze.enabled').value ?
+    return this.getPref('settings.a11y.face_gaze.enabled_sentinel').value ?
         this.i18n('deviceOn') :
         this.i18n('deviceOff');
   }
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/facegaze/constants.ts b/chrome/browser/resources/chromeos/accessibility/accessibility_common/facegaze/constants.ts
index 27c535d760ced..a2e5e72e749d0 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/facegaze/constants.ts
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/facegaze/constants.ts
@@ -11,9 +11,15 @@ export enum PrefNames {
   ACCELERATOR_DIALOG_HAS_BEEN_ACCEPTED =
       'settings.a11y.face_gaze.accelerator_dialog_has_been_accepted',
   ACTIONS_ENABLED = 'settings.a11y.face_gaze.actions_enabled',
+  ACTIONS_ENABLED_SENTINEL = 'settings.a11y.face_gaze.actions_enabled_sentinel',
   CURSOR_CONTROL_ENABLED = 'settings.a11y.face_gaze.cursor_control_enabled',
+  CURSOR_CONTROL_ENABLED_SENTINEL =
+      'settings.a11y.face_gaze.cursor_control_enabled_sentinel',
   CURSOR_USE_ACCELERATION = 'settings.a11y.face_gaze.cursor_use_acceleration',
   FACE_GAZE_ENABLED = 'settings.a11y.face_gaze.enabled',
+  FACE_GAZE_ENABLED_SENTINEL = 'settings.a11y.face_gaze.enabled_sentinel',
+  FACE_GAZE_ENABLED_SENTINEL_SHOW_DIALOG =
+      'settings.a11y.face_gaze.enabled_sentinel_show_dialog',
   GESTURE_TO_CONFIDENCE = 'settings.a11y.face_gaze.gestures_to_confidence',
   GESTURE_TO_KEY_COMBO = 'settings.a11y.face_gaze.gestures_to_key_combos',
   GESTURE_TO_MACRO = 'settings.a11y.face_gaze.gestures_to_macros',
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/facegaze/facegaze.ts b/chrome/browser/resources/chromeos/accessibility/accessibility_common/facegaze/facegaze.ts
index f8219bbad0990..58d031f0dca9c 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/facegaze/facegaze.ts
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/facegaze/facegaze.ts
@@ -115,8 +115,13 @@ export class FaceGaze {
     chrome.settingsPrivate.setPref(
         PrefNames.ACCELERATOR_DIALOG_HAS_BEEN_ACCEPTED, accepted);
     if (!accepted) {
-      // If the dialog was rejected, then disable the FaceGaze feature.
-      chrome.settingsPrivate.setPref(PrefNames.FACE_GAZE_ENABLED, false);
+      // If the dialog was rejected, then disable the FaceGaze feature and do
+      // not show the confirmation dialog for disabling.
+      chrome.settingsPrivate.setPref(
+          PrefNames.FACE_GAZE_ENABLED_SENTINEL_SHOW_DIALOG, false, undefined,
+          () => chrome.settingsPrivate.setPref(
+              PrefNames.FACE_GAZE_ENABLED_SENTINEL, false));
+
       return;
     }
 
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/facegaze/web_cam_face_landmarker.ts b/chrome/browser/resources/chromeos/accessibility/accessibility_common/facegaze/web_cam_face_landmarker.ts
index 1ddac6d3823d1..ff5cb5bab301c 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/facegaze/web_cam_face_landmarker.ts
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/facegaze/web_cam_face_landmarker.ts
@@ -88,7 +88,10 @@ export class WebCamFaceLandmarker {
             `Couldn't create FaceLandmarker because FaceGaze assets couldn't be
               installed.`);
 
-        chrome.settingsPrivate.setPref(PrefNames.FACE_GAZE_ENABLED, false);
+        chrome.settingsPrivate.setPref(
+            PrefNames.FACE_GAZE_ENABLED_SENTINEL_SHOW_DIALOG, false, undefined,
+            () => chrome.settingsPrivate.setPref(
+                PrefNames.FACE_GAZE_ENABLED_SENTINEL, false));
         return;
       }
 
diff --git a/chrome/test/data/webui/chromeos/settings/os_a11y_page/facegaze_actions_card_test.ts b/chrome/test/data/webui/chromeos/settings/os_a11y_page/facegaze_actions_card_test.ts
index 2449b6cf33e3f..9e86343197c33 100644
--- a/chrome/test/data/webui/chromeos/settings/os_a11y_page/facegaze_actions_card_test.ts
+++ b/chrome/test/data/webui/chromeos/settings/os_a11y_page/facegaze_actions_card_test.ts
@@ -149,8 +149,8 @@ suite('<facegaze-actions-card>', () => {
 
   test('actions enabled button syncs to pref', async () => {
     await initPage();
-    assertTrue(faceGazeActionsCard.prefs.settings.a11y.face_gaze.actions_enabled
-                   .value);
+    assertTrue(faceGazeActionsCard.prefs.settings.a11y.face_gaze
+                   .actions_enabled_sentinel.value);
 
     const button = faceGazeActionsCard.shadowRoot!
                        .querySelector<SettingsToggleButtonElement>(
@@ -164,7 +164,7 @@ suite('<facegaze-actions-card>', () => {
 
     assertFalse(button.checked);
     assertFalse(faceGazeActionsCard.prefs.settings.a11y.face_gaze
-                    .actions_enabled.value);
+                    .actions_enabled_sentinel.value);
   });
 
   test('actions disables controls if feature is disabled', async () => {
diff --git a/chrome/test/data/webui/chromeos/settings/os_a11y_page/facegaze_cursor_card_test.ts b/chrome/test/data/webui/chromeos/settings/os_a11y_page/facegaze_cursor_card_test.ts
index 098dc82948064..da01e0b28efcc 100644
--- a/chrome/test/data/webui/chromeos/settings/os_a11y_page/facegaze_cursor_card_test.ts
+++ b/chrome/test/data/webui/chromeos/settings/os_a11y_page/facegaze_cursor_card_test.ts
@@ -77,7 +77,7 @@ suite('<facegaze-cursor-card>', () => {
 
     const prefs = faceGazeCursorCard.prefs.settings.a11y.face_gaze;
 
-    assertTrue(prefs.cursor_control_enabled.value);
+    assertTrue(prefs.cursor_control_enabled_sentinel.value);
 
     const button = faceGazeCursorCard.shadowRoot!
                        .querySelector<SettingsToggleButtonElement>(
@@ -90,7 +90,7 @@ suite('<facegaze-cursor-card>', () => {
     flush();
 
     assertFalse(button.checked);
-    assertFalse(prefs.cursor_control_enabled.value);
+    assertFalse(prefs.cursor_control_enabled_sentinel.value);
   });
 
   test(
diff --git a/chrome/test/data/webui/chromeos/settings/os_a11y_page/facegaze_subpage_test.ts b/chrome/test/data/webui/chromeos/settings/os_a11y_page/facegaze_subpage_test.ts
index e906406051bf7..1d11459c4d2ae 100644
--- a/chrome/test/data/webui/chromeos/settings/os_a11y_page/facegaze_subpage_test.ts
+++ b/chrome/test/data/webui/chromeos/settings/os_a11y_page/facegaze_subpage_test.ts
@@ -71,10 +71,12 @@ suite('<settings-facegaze-subpage>', () => {
 
   test('toggle button reflects pref value', async () => {
     await initPage();
-    faceGazeSubpage.set('prefs.settings.a11y.face_gaze.enabled.value', true);
+    faceGazeSubpage.set(
+        'prefs.settings.a11y.face_gaze.enabled_sentinel.value', true);
     await flushTasks();
 
-    assertTrue(faceGazeSubpage.prefs.settings.a11y.face_gaze.enabled.value);
+    assertTrue(
+        faceGazeSubpage.prefs.settings.a11y.face_gaze.enabled_sentinel.value);
 
     const toggle = getToggleButton();
     assertTrue(!!toggle);
@@ -86,7 +88,8 @@ suite('<settings-facegaze-subpage>', () => {
   test('clicking toggle button updates pref value', async () => {
     await initPage();
 
-    assertFalse(faceGazeSubpage.prefs.settings.a11y.face_gaze.enabled.value);
+    assertFalse(
+        faceGazeSubpage.prefs.settings.a11y.face_gaze.enabled_sentinel.value);
 
     const toggle = getToggleButton();
     assertTrue(!!toggle);
@@ -98,7 +101,8 @@ suite('<settings-facegaze-subpage>', () => {
     await flushTasks();
 
     assertTrue(toggle.checked);
-    assertTrue(faceGazeSubpage.prefs.settings.a11y.face_gaze.enabled.value);
+    assertTrue(
+        faceGazeSubpage.prefs.settings.a11y.face_gaze.enabled_sentinel.value);
     assertEquals('On', toggle.label);
   });
 });