[go: nahoru, domu]

Add lock screen app window to lock screen focus cycle

Adds lock screen app windows to the lock screen focus cycle for views
based lock screen. The focus cycle is as follows:
Lock screen -> lock screen apps -> system shelf -> system tray

On lock screen apps side nothing changed, though views screen locker now
implements lock_screen_apps::FocusCyclerDelegate interface, and adds itself
to lock screen apps state as the focus cycler delegate implementation).
FocusCyclerDelegate is used by state controller to accept and hand off focus
between lock screen app windows and the rest of lock screen UI (and is
already in use for Web UI lock screen).

As lock_screen_apps::FocusCyclerDelegate, ViewsScreenLocker delegates
focus handout requests between lock_screen_apps::StateController and
LockContentsView using lock_screen mojo interface.

General flow is as follows:
1.  LockContentsView is notified that system tray or lock screen apps
    have lost focus.
2.  LockContentsView determines whether it is next in line to get focus
    *   if it is, it focuses the appropriate child
    *   if not, it passes focus on - either
            *   to lock screen apps using FocusLockScreenApps
                mojo message
            *   to system tray/shelf using shell's focus cycler
3.  Once the focus is leaving lock screen apps or system shelf/tray
    they notify LockScreenContents view the focus should be handed off:
    *   lock screen apps using HandleFocusLeavingLockScreenApps mojo
        request (which gets passed on to LockContentsView using
        LoginDataDispatcher)
    *   system tray/shelf using system tray notifier

BUG=746596

Change-Id: Ic4e1edd77193ae90203358671a2dd000c9aeefa2
Reviewed-on: https://chromium-review.googlesource.com/696673
Commit-Queue: Toni Barzic <tbarzic@chromium.org>
Reviewed-by: James Cook <jamescook@chromium.org>
Reviewed-by: Tom Sepez <tsepez@chromium.org>
Reviewed-by: Jacob Dufault <jdufault@chromium.org>
Cr-Commit-Position: refs/heads/master@{#506640}
diff --git a/ash/BUILD.gn b/ash/BUILD.gn
index fa3d1716..678b376e 100644
--- a/ash/BUILD.gn
+++ b/ash/BUILD.gn
@@ -216,6 +216,7 @@
     "laser/laser_pointer_view.h",
     "laser/laser_segment_utils.cc",
     "laser/laser_segment_utils.h",
+    "login/lock_screen_apps_focus_observer.h",
     "login/lock_screen_controller.cc",
     "login/lock_screen_controller.h",
     "login/ui/layout_util.cc",
diff --git a/ash/login/lock_screen_apps_focus_observer.h b/ash/login/lock_screen_apps_focus_observer.h
new file mode 100644
index 0000000..0383385
--- /dev/null
+++ b/ash/login/lock_screen_apps_focus_observer.h
@@ -0,0 +1,25 @@
+// 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_LOGIN_LOCK_SCREEN_APPS_FOCUS_OBSERVER_H_
+#define ASH_LOGIN_LOCK_SCREEN_APPS_FOCUS_OBSERVER_H_
+
+#include "ash/ash_export.h"
+
+namespace ash {
+
+// An interface used to observe lock screen apps focus related events, as
+// reported through lock screen mojo interface.
+class ASH_EXPORT LockScreenAppsFocusObserver {
+ public:
+  virtual ~LockScreenAppsFocusObserver() {}
+
+  // Called when focus is leaving a lock screen app window due to tabbing.
+  // |reverse| - whether the tab order is reversed.
+  virtual void OnFocusLeavingLockScreenApps(bool reverse) = 0;
+};
+
+}  // namespace ash
+
+#endif  // ASH_LOGIN_LOCK_SCREEN_APPS_FOCUS_OBSERVER_H_
diff --git a/ash/login/lock_screen_controller.cc b/ash/login/lock_screen_controller.cc
index 4e25b6f..5e9de90f 100644
--- a/ash/login/lock_screen_controller.cc
+++ b/ash/login/lock_screen_controller.cc
@@ -4,6 +4,7 @@
 
 #include "ash/login/lock_screen_controller.h"
 
+#include "ash/login/lock_screen_apps_focus_observer.h"
 #include "ash/login/ui/lock_screen.h"
 #include "ash/login/ui/login_data_dispatcher.h"
 #include "ash/public/cpp/ash_pref_names.h"
@@ -122,6 +123,11 @@
       &LockScreenController::OnGetSystemSalt, base::Unretained(this)));
 }
 
+void LockScreenController::HandleFocusLeavingLockScreenApps(bool reverse) {
+  for (auto& observer : lock_screen_apps_focus_observers_)
+    observer.OnFocusLeavingLockScreenApps(reverse);
+}
+
 void LockScreenController::AttemptUnlock(const AccountId& account_id) {
   if (!lock_screen_client_)
     return;
@@ -177,6 +183,26 @@
   lock_screen_client_->OnMaxIncorrectPasswordAttempted(account_id);
 }
 
+void LockScreenController::FocusLockScreenApps(bool reverse) {
+  if (!lock_screen_client_)
+    return;
+  lock_screen_client_->FocusLockScreenApps(reverse);
+}
+
+void LockScreenController::AddLockScreenAppsFocusObserver(
+    LockScreenAppsFocusObserver* observer) {
+  lock_screen_apps_focus_observers_.AddObserver(observer);
+}
+
+void LockScreenController::RemoveLockScreenAppsFocusObserver(
+    LockScreenAppsFocusObserver* observer) {
+  lock_screen_apps_focus_observers_.RemoveObserver(observer);
+}
+
+void LockScreenController::FlushForTesting() {
+  lock_screen_client_.FlushForTesting();
+}
+
 void LockScreenController::DoAuthenticateUser(
     const AccountId& account_id,
     const std::string& password,
diff --git a/ash/login/lock_screen_controller.h b/ash/login/lock_screen_controller.h
index 4cb9567..befb5fd3 100644
--- a/ash/login/lock_screen_controller.h
+++ b/ash/login/lock_screen_controller.h
@@ -8,12 +8,14 @@
 #include "ash/ash_export.h"
 #include "ash/public/interfaces/lock_screen.mojom.h"
 #include "base/macros.h"
+#include "base/observer_list.h"
 #include "mojo/public/cpp/bindings/binding_set.h"
 
 class PrefRegistrySimple;
 
 namespace ash {
 
+class LockScreenAppsFocusObserver;
 class LoginDataDispatcher;
 
 // LockScreenController implements mojom::LockScreen and wraps the
@@ -51,6 +53,7 @@
                  bool show_guest) override;
   void SetPinEnabledForUser(const AccountId& account_id,
                             bool is_enabled) override;
+  void HandleFocusLeavingLockScreenApps(bool reverse) override;
 
   // Wrappers around the mojom::LockScreenClient interface.
   // Hash the password and send AuthenticateUser request to LockScreenClient.
@@ -71,6 +74,16 @@
   void SignOutUser();
   void CancelAddUser();
   void OnMaxIncorrectPasswordAttempted(const AccountId& account_id);
+  void FocusLockScreenApps(bool reverse);
+
+  // Methods to manage lock screen apps focus observers.
+  // The observers will be notified when lock screen apps focus changes are
+  // reported via lock screen mojo interface.
+  void AddLockScreenAppsFocusObserver(LockScreenAppsFocusObserver* observer);
+  void RemoveLockScreenAppsFocusObserver(LockScreenAppsFocusObserver* observer);
+
+  // Flushes the mojo pipes - to be used in tests.
+  void FlushForTesting();
 
  private:
   using PendingAuthenticateUserCall =
@@ -97,6 +110,9 @@
   // User authentication call that will run when we have system salt.
   PendingAuthenticateUserCall pending_user_auth_;
 
+  base::ObserverList<LockScreenAppsFocusObserver>
+      lock_screen_apps_focus_observers_;
+
   DISALLOW_COPY_AND_ASSIGN(LockScreenController);
 };
 
diff --git a/ash/login/mock_lock_screen_client.h b/ash/login/mock_lock_screen_client.h
index a0867ed..85fec52a 100644
--- a/ash/login/mock_lock_screen_client.h
+++ b/ash/login/mock_lock_screen_client.h
@@ -44,6 +44,7 @@
   MOCK_METHOD0(CancelAddUser, void());
   MOCK_METHOD1(OnMaxIncorrectPasswordAttempted,
                void(const AccountId& account_id));
+  MOCK_METHOD1(FocusLockScreenApps, void(bool reverse));
 
  private:
   bool authenticate_user_callback_result_ = true;
diff --git a/ash/login/ui/lock_contents_view.cc b/ash/login/ui/lock_contents_view.cc
index 4e16703..2210885 100644
--- a/ash/login/ui/lock_contents_view.cc
+++ b/ash/login/ui/lock_contents_view.cc
@@ -170,6 +170,7 @@
       display_observer_(this) {
   data_dispatcher_->AddObserver(this);
   display_observer_.Add(display::Screen::GetScreen());
+  Shell::Get()->lock_screen_controller()->AddLockScreenAppsFocusObserver(this);
   Shell::Get()->system_tray_notifier()->AddSystemTrayFocusObserver(this);
   error_bubble_ = base::MakeUnique<LoginBubble>();
 
@@ -186,10 +187,14 @@
   note_action_ =
       new NoteActionLaunchButton(initial_note_action_state, data_dispatcher_);
   AddChildView(note_action_);
+
+  OnLockScreenNoteStateChanged(initial_note_action_state);
 }
 
 LockContentsView::~LockContentsView() {
   data_dispatcher_->RemoveObserver(this);
+  Shell::Get()->lock_screen_controller()->RemoveLockScreenAppsFocusObserver(
+      this);
   Shell::Get()->system_tray_notifier()->RemoveSystemTrayFocusObserver(this);
 }
 
@@ -228,7 +233,12 @@
 void LockContentsView::AboutToRequestFocusFromTabTraversal(bool reverse) {
   // The LockContentsView itself doesn't have anything to focus. If it gets
   // focused we should change the currently focused widget (ie, to the shelf or
-  // status area).
+  // status area, or lock screen apps, if they are active).
+  if (reverse && lock_screen_apps_active_) {
+    Shell::Get()->lock_screen_controller()->FocusLockScreenApps(reverse);
+    return;
+  }
+
   FocusNextWidget(reverse);
 }
 
@@ -305,10 +315,38 @@
   }
 }
 
+void LockContentsView::OnLockScreenNoteStateChanged(
+    mojom::TrayActionState state) {
+  bool old_lock_screen_apps_active = lock_screen_apps_active_;
+  lock_screen_apps_active_ = state == mojom::TrayActionState::kActive;
+
+  // If lock screen apps just got deactivated - request focus for primary auth,
+  // which should focus the password field.
+  if (old_lock_screen_apps_active && !lock_screen_apps_active_ && primary_auth_)
+    primary_auth_->RequestFocus();
+}
+
+void LockContentsView::OnFocusLeavingLockScreenApps(bool reverse) {
+  if (!reverse || lock_screen_apps_active_)
+    FocusNextWidget(reverse);
+  else
+    FindFirstOrLastFocusableChild(this, reverse)->RequestFocus();
+}
+
 void LockContentsView::OnFocusLeavingSystemTray(bool reverse) {
   // This function is called when the system tray is losing focus. We want to
-  // focus the first or last child in this view.
+  // focus the first or last child in this view, or a lock screen app window if
+  // one is active (in which case lock contents should not have focus). In the
+  // later case, still focus lock screen first, to synchronously take focus away
+  // from the system shelf (or tray) - lock shelf view expect the focus to be
+  // taken when it passes it to lock screen view, and can misbehave in case the
+  // focus is kept in it.
   FindFirstOrLastFocusableChild(this, reverse)->RequestFocus();
+
+  if (lock_screen_apps_active_) {
+    Shell::Get()->lock_screen_controller()->FocusLockScreenApps(reverse);
+    return;
+  }
 }
 
 void LockContentsView::OnDisplayMetricsChanged(const display::Display& display,
diff --git a/ash/login/ui/lock_contents_view.h b/ash/login/ui/lock_contents_view.h
index 30203e0..f7f4a299 100644
--- a/ash/login/ui/lock_contents_view.h
+++ b/ash/login/ui/lock_contents_view.h
@@ -10,6 +10,7 @@
 #include <vector>
 
 #include "ash/ash_export.h"
+#include "ash/login/lock_screen_apps_focus_observer.h"
 #include "ash/login/ui/login_data_dispatcher.h"
 #include "ash/login/ui/non_accessible_view.h"
 #include "ash/system/system_tray_focus_observer.h"
@@ -41,6 +42,7 @@
 // but it is always shown on the primary display. There is only one instance
 // at a time.
 class ASH_EXPORT LockContentsView : public NonAccessibleView,
+                                    public LockScreenAppsFocusObserver,
                                     public LoginDataDispatcher::Observer,
                                     public SystemTrayFocusObserver,
                                     public display::DisplayObserver {
@@ -70,10 +72,14 @@
   void OnFocus() override;
   void AboutToRequestFocusFromTabTraversal(bool reverse) override;
 
+  // LockScreenAppsFocusObserver:
+  void OnFocusLeavingLockScreenApps(bool reverse) override;
+
   // LoginDataDispatcher::Observer:
   void OnUsersChanged(
       const std::vector<mojom::LoginUserInfoPtr>& users) override;
   void OnPinEnabledForUserChanged(const AccountId& user, bool enabled) override;
+  void OnLockScreenNoteStateChanged(mojom::TrayActionState state) override;
 
   // SystemTrayFocusObserver:
   void OnFocusLeavingSystemTray(bool reverse) override;
@@ -178,6 +184,10 @@
   std::unique_ptr<LoginBubble> error_bubble_;
   int unlock_attempt_ = 0;
 
+  // Whether a lock screen app is currently active (i.e. lock screen note action
+  // state is reported as kActive by the data dispatcher).
+  bool lock_screen_apps_active_ = false;
+
   DISALLOW_COPY_AND_ASSIGN(LockContentsView);
 };
 
diff --git a/ash/login/ui/lock_contents_view_unittest.cc b/ash/login/ui/lock_contents_view_unittest.cc
index 4401df1..7db9047 100644
--- a/ash/login/ui/lock_contents_view_unittest.cc
+++ b/ash/login/ui/lock_contents_view_unittest.cc
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include <memory>
 #include <unordered_set>
 
 #include "ash/login/ui/lock_contents_view.h"
@@ -28,7 +29,7 @@
   auto* contents = new LockContentsView(mojom::TrayActionState::kNotAvailable,
                                         data_dispatcher());
   SetUserCount(1);
-  ShowWidgetWithContent(contents);
+  std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(contents);
 
   // Verify user list and secondary auth are not shown for one user.
   LockContentsView::TestApi test_api(contents);
@@ -72,11 +73,11 @@
   auto* contents = new LockContentsView(mojom::TrayActionState::kNotAvailable,
                                         data_dispatcher());
   SetUserCount(1);
-  ShowWidgetWithContent(contents);
+  std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(contents);
 
   LockContentsView::TestApi test_api(contents);
   LoginAuthUserView* auth_view = test_api.primary_auth();
-  gfx::Rect widget_bounds = widget()->GetWindowBoundsInScreen();
+  gfx::Rect widget_bounds = widget->GetWindowBoundsInScreen();
   int expected_margin =
       (widget_bounds.width() - auth_view->GetPreferredSize().width()) / 2;
   gfx::Rect auth_bounds = auth_view->GetBoundsInScreen();
@@ -93,11 +94,11 @@
   auto* contents = new LockContentsView(mojom::TrayActionState::kAvailable,
                                         data_dispatcher());
   SetUserCount(1);
-  ShowWidgetWithContent(contents);
+  std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(contents);
 
   LockContentsView::TestApi test_api(contents);
   LoginAuthUserView* auth_view = test_api.primary_auth();
-  gfx::Rect widget_bounds = widget()->GetWindowBoundsInScreen();
+  gfx::Rect widget_bounds = widget->GetWindowBoundsInScreen();
   int expected_margin =
       (widget_bounds.width() - auth_view->GetPreferredSize().width()) / 2;
   gfx::Rect auth_bounds = auth_view->GetBoundsInScreen();
@@ -117,7 +118,7 @@
                                         data_dispatcher());
   LockContentsView::TestApi test_api(contents);
   SetUserCount(3);
-  ShowWidgetWithContent(contents);
+  std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(contents);
 
   // Returns the distance between the auth user view and the user view.
   auto calculate_distance = [&]() {
@@ -131,7 +132,7 @@
 
   const display::Display& display =
       display::Screen::GetScreen()->GetDisplayNearestWindow(
-          widget()->GetNativeWindow());
+          widget->GetNativeWindow());
   for (int i = 2; i < 10; ++i) {
     SetUserCount(i);
 
@@ -166,7 +167,7 @@
                                         data_dispatcher());
   LockContentsView::TestApi test_api(contents);
   SetUserCount(2);
-  ShowWidgetWithContent(contents);
+  std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(contents);
 
   // Capture user info to validate it did not change during the swap.
   AccountId primary_user =
@@ -212,7 +213,7 @@
   LockContentsView::TestApi test_api(contents);
   SetUserCount(5);
   EXPECT_EQ(users().size() - 1, test_api.user_views().size());
-  ShowWidgetWithContent(contents);
+  std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(contents);
 
   LoginAuthUserView* auth_view = test_api.primary_auth();
 
@@ -249,7 +250,7 @@
   auto* contents = new LockContentsView(mojom::TrayActionState::kNotAvailable,
                                         data_dispatcher());
   SetUserCount(1);
-  ShowWidgetWithContent(contents);
+  std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(contents);
 
   LockContentsView::TestApi test_api(contents);
 
@@ -263,7 +264,7 @@
   EXPECT_TRUE(test_api.note_action()->visible());
 
   // Verify the bounds of the note action button are as expected.
-  gfx::Rect widget_bounds = widget()->GetWindowBoundsInScreen();
+  gfx::Rect widget_bounds = widget->GetWindowBoundsInScreen();
   gfx::Size note_action_size = test_api.note_action()->GetPreferredSize();
   EXPECT_EQ(gfx::Rect(widget_bounds.top_right() -
                           gfx::Vector2d(note_action_size.width(), 0),
@@ -283,14 +284,14 @@
   auto* contents = new LockContentsView(mojom::TrayActionState::kAvailable,
                                         data_dispatcher());
   SetUserCount(1);
-  ShowWidgetWithContent(contents);
+  std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(contents);
 
   LockContentsView::TestApi test_api(contents);
 
   // Verify the note action button is visible and positioned in the top rigth
   // corner of the screen.
   EXPECT_TRUE(test_api.note_action()->visible());
-  gfx::Rect widget_bounds = widget()->GetWindowBoundsInScreen();
+  gfx::Rect widget_bounds = widget->GetWindowBoundsInScreen();
   gfx::Size note_action_size = test_api.note_action()->GetPreferredSize();
   EXPECT_EQ(gfx::Rect(widget_bounds.top_right() -
                           gfx::Vector2d(note_action_size.width(), 0),
diff --git a/ash/login/ui/lock_screen_sanity_unittest.cc b/ash/login/ui/lock_screen_sanity_unittest.cc
index c50030cd..e14cdf3 100644
--- a/ash/login/ui/lock_screen_sanity_unittest.cc
+++ b/ash/login/ui/lock_screen_sanity_unittest.cc
@@ -2,6 +2,9 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include <memory>
+
+#include "ash/login/lock_screen_controller.h"
 #include "ash/login/mock_lock_screen_client.h"
 #include "ash/login/ui/lock_contents_view.h"
 #include "ash/login/ui/login_test_base.h"
@@ -16,13 +19,35 @@
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/events/test/event_generator.h"
 #include "ui/views/focus/focus_manager.h"
+#include "ui/views/widget/widget.h"
 
 using ::testing::_;
+using ::testing::Invoke;
 using LockScreenSanityTest = ash::LoginTestBase;
 
 namespace ash {
 namespace {
 
+class LockScreenAppFocuser {
+ public:
+  explicit LockScreenAppFocuser(views::Widget* lock_screen_app_widget)
+      : lock_screen_app_widget_(lock_screen_app_widget) {}
+  ~LockScreenAppFocuser() = default;
+
+  bool reversed_tab_order() const { return reversed_tab_order_; }
+
+  void FocusLockScreenApp(bool reverse) {
+    reversed_tab_order_ = reverse;
+    lock_screen_app_widget_->Activate();
+  }
+
+ private:
+  bool reversed_tab_order_ = false;
+  views::Widget* lock_screen_app_widget_;
+
+  DISALLOW_COPY_AND_ASSIGN(LockScreenAppFocuser);
+};
+
 // Returns true if |view| or any child of it has focus.
 bool HasFocusInAnyChildView(views::View* view) {
   if (view->HasFocus())
@@ -34,14 +59,41 @@
   return false;
 }
 
-void ExpectFocused(views::View* view) {
-  EXPECT_TRUE(view->GetWidget()->IsActive());
-  EXPECT_TRUE(HasFocusInAnyChildView(view));
+// Keeps tabbing through |view| until the view loses focus.
+// The number of generated tab events will be limited - if the focus is still
+// within the view by the time the limit is hit, this will return false.
+bool TabThroughView(ui::test::EventGenerator* event_generator,
+                    views::View* view,
+                    bool reverse) {
+  if (!HasFocusInAnyChildView(view)) {
+    ADD_FAILURE() << "View not focused initially.";
+    return false;
+  }
+
+  for (int i = 0; i < 50; ++i) {
+    event_generator->PressKey(ui::KeyboardCode::VKEY_TAB,
+                              reverse ? ui::EF_SHIFT_DOWN : 0);
+    if (!HasFocusInAnyChildView(view))
+      return true;
+  }
+
+  return false;
 }
 
-void ExpectNotFocused(views::View* view) {
-  EXPECT_FALSE(view->GetWidget()->IsActive());
-  EXPECT_FALSE(HasFocusInAnyChildView(view));
+testing::AssertionResult VerifyFocused(views::View* view) {
+  if (!view->GetWidget()->IsActive())
+    return testing::AssertionFailure() << "Widget not active.";
+  if (!HasFocusInAnyChildView(view))
+    return testing::AssertionFailure() << "No focused descendant.";
+  return testing::AssertionSuccess();
+}
+
+testing::AssertionResult VerifyNotFocused(views::View* view) {
+  if (view->GetWidget()->IsActive())
+    return testing::AssertionFailure() << "Widget active";
+  if (HasFocusInAnyChildView(view))
+    return testing::AssertionFailure() << "Has focused descendant.";
+  return testing::AssertionSuccess();
 }
 
 }  // namespace
@@ -55,7 +107,7 @@
   // The lock screen requires at least one user.
   SetUserCount(1);
 
-  ShowWidgetWithContent(contents);
+  std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(contents);
 
   // Textfield should have focus.
   EXPECT_EQ(MakeLoginPasswordTestApi(contents).textfield(),
@@ -75,7 +127,7 @@
   // The lock screen requires at least one user.
   SetUserCount(1);
 
-  ShowWidgetWithContent(contents);
+  std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(contents);
 
   // Password submit runs mojo.
   std::unique_ptr<MockLockScreenClient> client = BindMockLockScreenClient();
@@ -100,28 +152,24 @@
   auto* lock = new LockContentsView(mojom::TrayActionState::kNotAvailable,
                                     data_dispatcher());
   SetUserCount(1);
-  ShowWidgetWithContent(lock);
+  std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(lock);
   views::View* shelf = Shelf::ForWindow(lock->GetWidget()->GetNativeWindow())
                            ->shelf_widget()
                            ->GetContentsView();
 
   // Lock has focus.
-  ExpectFocused(lock);
-  ExpectNotFocused(shelf);
+  EXPECT_TRUE(VerifyFocused(lock));
+  EXPECT_TRUE(VerifyNotFocused(shelf));
 
   // Tab (eventually) goes to the shelf.
-  for (int i = 0; i < 50; ++i) {
-    GetEventGenerator().PressKey(ui::KeyboardCode::VKEY_TAB, 0);
-    if (!HasFocusInAnyChildView(lock))
-      break;
-  }
-  ExpectNotFocused(lock);
-  ExpectFocused(shelf);
+  ASSERT_TRUE(TabThroughView(&GetEventGenerator(), lock, false /*reverse*/));
+  EXPECT_TRUE(VerifyNotFocused(lock));
+  EXPECT_TRUE(VerifyFocused(shelf));
 
   // A single shift+tab brings focus back to the lock screen.
   GetEventGenerator().PressKey(ui::KeyboardCode::VKEY_TAB, ui::EF_SHIFT_DOWN);
-  ExpectFocused(lock);
-  ExpectNotFocused(shelf);
+  EXPECT_TRUE(VerifyFocused(lock));
+  EXPECT_TRUE(VerifyNotFocused(shelf));
 }
 
 // Verifies that shift-tabbing from the lock screen will eventually focus the
@@ -135,7 +183,7 @@
   auto* lock = new LockContentsView(mojom::TrayActionState::kNotAvailable,
                                     data_dispatcher());
   SetUserCount(1);
-  ShowWidgetWithContent(lock);
+  std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(lock);
   views::View* status_area =
       RootWindowController::ForWindow(lock->GetWidget()->GetNativeWindow())
           ->GetSystemTray()
@@ -143,8 +191,8 @@
           ->GetContentsView();
 
   // Lock screen has focus.
-  ExpectFocused(lock);
-  ExpectNotFocused(status_area);
+  EXPECT_TRUE(VerifyFocused(lock));
+  EXPECT_TRUE(VerifyNotFocused(status_area));
 
   // Two shift+tab bring focus to the status area.
   // TODO(crbug.com/768076): Only one shift+tab is needed as the focus should
@@ -156,13 +204,122 @@
   // Focus from user view to the status area.
   GetEventGenerator().PressKey(ui::KeyboardCode::VKEY_TAB, ui::EF_SHIFT_DOWN);
 
-  ExpectNotFocused(lock);
-  ExpectFocused(status_area);
+  EXPECT_TRUE(VerifyNotFocused(lock));
+  EXPECT_TRUE(VerifyFocused(status_area));
 
   // A single tab brings focus back to the lock screen.
   GetEventGenerator().PressKey(ui::KeyboardCode::VKEY_TAB, 0);
-  ExpectFocused(lock);
-  ExpectNotFocused(status_area);
+  EXPECT_TRUE(VerifyFocused(lock));
+  EXPECT_TRUE(VerifyNotFocused(status_area));
+}
+
+TEST_F(LockScreenSanityTest, TabWithLockScreenAppActive) {
+  GetSessionControllerClient()->SetSessionState(
+      session_manager::SessionState::LOCKED);
+
+  auto* lock = new LockContentsView(mojom::TrayActionState::kNotAvailable,
+                                    data_dispatcher());
+  SetUserCount(1);
+  std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(lock);
+
+  views::View* shelf = Shelf::ForWindow(lock->GetWidget()->GetNativeWindow())
+                           ->shelf_widget()
+                           ->GetContentsView();
+
+  views::View* status_area =
+      RootWindowController::ForWindow(lock->GetWidget()->GetNativeWindow())
+          ->GetSystemTray()
+          ->GetWidget()
+          ->GetContentsView();
+
+  LockScreenController* lock_screen_controller =
+      Shell::Get()->lock_screen_controller();
+
+  // Initialize lock screen action state.
+  data_dispatcher()->SetLockScreenNoteState(mojom::TrayActionState::kActive);
+
+  // Create and focus a lock screen app window.
+  auto* lock_screen_app = new views::View();
+  lock_screen_app->SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
+  std::unique_ptr<views::Widget> app_widget =
+      CreateWidgetWithContent(lock_screen_app);
+  app_widget->Show();
+
+  // Lock screen app focus is requested using lock screen mojo client - set up
+  // the mock client.
+  LockScreenAppFocuser app_widget_focuser(app_widget.get());
+  std::unique_ptr<MockLockScreenClient> client = BindMockLockScreenClient();
+  EXPECT_CALL(*client, FocusLockScreenApps(_))
+      .WillRepeatedly(Invoke(&app_widget_focuser,
+                             &LockScreenAppFocuser::FocusLockScreenApp));
+
+  // Initially, focus should be with the lock screen app - when the app loses
+  // focus (notified via mojo interface), shelf should get the focus next.
+  EXPECT_TRUE(VerifyFocused(lock_screen_app));
+  lock_screen_controller->HandleFocusLeavingLockScreenApps(false /*reverse*/);
+  EXPECT_TRUE(VerifyFocused(shelf));
+
+  // Reversing focus should bring focus back to the lock screen app.
+  GetEventGenerator().PressKey(ui::KeyboardCode::VKEY_TAB, ui::EF_SHIFT_DOWN);
+  // Focus is passed to lock screen apps via mojo - flush the request.
+  lock_screen_controller->FlushForTesting();
+  EXPECT_TRUE(VerifyFocused(lock_screen_app));
+  EXPECT_TRUE(app_widget_focuser.reversed_tab_order());
+
+  // Have the app tab out in reverse tab order - in this case, the status area
+  // should get the focus.
+  lock_screen_controller->HandleFocusLeavingLockScreenApps(true /*reverse*/);
+  EXPECT_TRUE(VerifyFocused(status_area));
+
+  // Tabbing out of the status area (in default order) should focus the lock
+  // screen app again.
+  GetEventGenerator().PressKey(ui::KeyboardCode::VKEY_TAB, 0);
+  // Focus is passed to lock screen apps via mojo - flush the request.
+  lock_screen_controller->FlushForTesting();
+  EXPECT_TRUE(VerifyFocused(lock_screen_app));
+  EXPECT_FALSE(app_widget_focuser.reversed_tab_order());
+
+  // Tab out of the lock screen app once more - the shelf should get the focus
+  // again.
+  lock_screen_controller->HandleFocusLeavingLockScreenApps(false /*reverse*/);
+  EXPECT_TRUE(VerifyFocused(shelf));
+}
+
+TEST_F(LockScreenSanityTest, FocusLockScreenWhenLockScreenAppExit) {
+  // Set up lock screen.
+  GetSessionControllerClient()->SetSessionState(
+      session_manager::SessionState::LOCKED);
+  auto* lock = new LockContentsView(mojom::TrayActionState::kNotAvailable,
+                                    data_dispatcher());
+  SetUserCount(1);
+  std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(lock);
+
+  views::View* shelf = Shelf::ForWindow(lock->GetWidget()->GetNativeWindow())
+                           ->shelf_widget()
+                           ->GetContentsView();
+
+  // Setup and focus a lock screen app.
+  data_dispatcher()->SetLockScreenNoteState(mojom::TrayActionState::kActive);
+  auto* lock_screen_app = new views::View();
+  lock_screen_app->SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
+  std::unique_ptr<views::Widget> app_widget =
+      CreateWidgetWithContent(lock_screen_app);
+  app_widget->Show();
+  EXPECT_TRUE(VerifyFocused(lock_screen_app));
+
+  // Tab out of the lock screen app - shelf should get the focus.
+  Shell::Get()->lock_screen_controller()->HandleFocusLeavingLockScreenApps(
+      false /*reverse*/);
+  EXPECT_TRUE(VerifyFocused(shelf));
+
+  // Move the lock screen note taking to available state (which happens when the
+  // app session ends) - this should focus the lock screen.
+  data_dispatcher()->SetLockScreenNoteState(mojom::TrayActionState::kAvailable);
+  EXPECT_TRUE(VerifyFocused(lock));
+
+  // Tab through the lock screen - the focus should eventually get to the shelf.
+  ASSERT_TRUE(TabThroughView(&GetEventGenerator(), lock, false /*reverse*/));
+  EXPECT_TRUE(VerifyFocused(shelf));
 }
 
 }  // namespace ash
diff --git a/ash/login/ui/login_auth_user_view_unittest.cc b/ash/login/ui/login_auth_user_view_unittest.cc
index e5c31fa5..ed31636 100644
--- a/ash/login/ui/login_auth_user_view_unittest.cc
+++ b/ash/login/ui/login_auth_user_view_unittest.cc
@@ -6,6 +6,7 @@
 #include "ash/login/ui/login_test_base.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/views/layout/box_layout.h"
+#include "ui/views/widget/widget.h"
 
 namespace ash {
 
@@ -30,7 +31,7 @@
     container_->SetLayoutManager(
         new views::BoxLayout(views::BoxLayout::kVertical));
     container_->AddChildView(view_);
-    ShowWidgetWithContent(container_);
+    SetWidget(CreateWidgetWithContent(container_));
   }
 
   mojom::LoginUserInfoPtr user_;
@@ -76,4 +77,4 @@
   EXPECT_TRUE(user_test.is_opaque());
 }
 
-}  // namespace ash
\ No newline at end of file
+}  // namespace ash
diff --git a/ash/login/ui/login_bubble_unittest.cc b/ash/login/ui/login_bubble_unittest.cc
index 1de9292..88c878f 100644
--- a/ash/login/ui/login_bubble_unittest.cc
+++ b/ash/login/ui/login_bubble_unittest.cc
@@ -7,6 +7,7 @@
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/events/test/event_generator.h"
 #include "ui/views/layout/box_layout.h"
+#include "ui/views/widget/widget.h"
 
 namespace ash {
 
@@ -51,7 +52,7 @@
 
     container_->AddChildView(bubble_opener_);
     container_->AddChildView(other_view_);
-    ShowWidgetWithContent(container_);
+    SetWidget(CreateWidgetWithContent(container_));
 
     bubble_ = base::MakeUnique<LoginBubble>();
   }
diff --git a/ash/login/ui/login_password_view_test.cc b/ash/login/ui/login_password_view_test.cc
index 90b77422..fe09c18 100644
--- a/ash/login/ui/login_password_view_test.cc
+++ b/ash/login/ui/login_password_view_test.cc
@@ -9,6 +9,7 @@
 #include "ash/shell.h"
 #include "base/strings/utf_string_conversions.h"
 #include "ui/events/test/event_generator.h"
+#include "ui/views/widget/widget.h"
 
 namespace ash {
 
@@ -28,7 +29,7 @@
                            base::Unretained(this)),
                 base::Bind(&LoginPasswordViewTest::OnPasswordTextChanged,
                            base::Unretained(this)));
-    ShowWidgetWithContent(view_);
+    SetWidget(CreateWidgetWithContent(view_));
   }
 
   // Called when a password is submitted.
diff --git a/ash/login/ui/login_pin_view_unittest.cc b/ash/login/ui/login_pin_view_unittest.cc
index 7327f607..0f6201b 100644
--- a/ash/login/ui/login_pin_view_unittest.cc
+++ b/ash/login/ui/login_pin_view_unittest.cc
@@ -8,6 +8,7 @@
 #include "ash/login/ui/login_test_base.h"
 #include "base/timer/mock_timer.h"
 #include "ui/events/test/event_generator.h"
+#include "ui/views/widget/widget.h"
 
 namespace ash {
 
@@ -26,7 +27,7 @@
         base::Bind(&LoginPinViewTest::OnPinKey, base::Unretained(this)),
         base::Bind(&LoginPinViewTest::OnPinBackspace, base::Unretained(this)));
 
-    ShowWidgetWithContent(view_);
+    SetWidget(CreateWidgetWithContent(view_));
   }
 
   // Called when a password is submitted.
@@ -156,4 +157,4 @@
   EXPECT_EQ(0, backspace_);
 }
 
-}  // namespace ash
\ No newline at end of file
+}  // namespace ash
diff --git a/ash/login/ui/login_test_base.cc b/ash/login/ui/login_test_base.cc
index 705e400..7413569 100644
--- a/ash/login/ui/login_test_base.cc
+++ b/ash/login/ui/login_test_base.cc
@@ -26,6 +26,7 @@
   ~WidgetDelegate() override = default;
 
   // views::WidgetDelegate:
+  void DeleteDelegate() override { delete this; }
   views::View* GetInitiallyFocusedView() override { return content_; }
   views::Widget* GetWidget() override { return content_->GetWidget(); }
   const views::Widget* GetWidget() const override {
@@ -42,16 +43,19 @@
 
 LoginTestBase::~LoginTestBase() = default;
 
-void LoginTestBase::ShowWidgetWithContent(views::View* content) {
-  EXPECT_FALSE(widget_) << "CreateWidget can only be called once.";
+void LoginTestBase::SetWidget(std::unique_ptr<views::Widget> widget) {
+  EXPECT_FALSE(widget_) << "SetWidget can only be called once.";
+  widget_ = std::move(widget);
+}
 
-  delegate_ = base::MakeUnique<WidgetDelegate>(content);
-
+std::unique_ptr<views::Widget> LoginTestBase::CreateWidgetWithContent(
+    views::View* content) {
   views::Widget::InitParams params(
       views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
+  params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
   params.context = CurrentContext();
   params.bounds = gfx::Rect(0, 0, 800, 800);
-  params.delegate = delegate_.get();
+  params.delegate = new WidgetDelegate(content);
 
   // Set the widget to the lock screen container, since a test may change the
   // session state to locked, which will hide all widgets not associated with
@@ -59,11 +63,11 @@
   params.parent = Shell::GetContainer(Shell::GetPrimaryRootWindow(),
                                       kShellWindowId_LockScreenContainer);
 
-  widget_ = new views::Widget();
-  widget_->Init(params);
-  widget_->SetContentsView(content);
-  widget_->Show();
-  ASSERT_TRUE(widget()->IsActive());
+  auto new_widget = std::make_unique<views::Widget>();
+  new_widget->Init(params);
+  new_widget->SetContentsView(content);
+  new_widget->Show();
+  return new_widget;
 }
 
 mojom::LoginUserInfoPtr LoginTestBase::CreateUser(
@@ -96,10 +100,7 @@
 }
 
 void LoginTestBase::TearDown() {
-  if (widget_) {
-    widget_->Close();
-    widget_ = nullptr;
-  }
+  widget_.reset();
 
   AshTestBase::TearDown();
 }
diff --git a/ash/login/ui/login_test_base.h b/ash/login/ui/login_test_base.h
index edb5c98..8d61521 100644
--- a/ash/login/ui/login_test_base.h
+++ b/ash/login/ui/login_test_base.h
@@ -5,6 +5,8 @@
 #ifndef ASH_LOGIN_UI_LOGIN_TEST_BASE_H_
 #define ASH_LOGIN_UI_LOGIN_TEST_BASE_H_
 
+#include <memory>
+
 #include "ash/login/ui/login_data_dispatcher.h"
 #include "ash/public/interfaces/login_user_info.mojom.h"
 #include "ash/test/ash_test_base.h"
@@ -24,10 +26,16 @@
   LoginTestBase();
   ~LoginTestBase() override;
 
-  // Creates and displays a widget containing |content|.
-  void ShowWidgetWithContent(views::View* content);
+  // Sets the primary test widget. The widget can be retrieved using |widget()|.
+  // This can be used to make a wdiget scoped to the whole test, e.g. if the
+  // widget is created in a SetUp override.
+  // May be called at most once.
+  void SetWidget(std::unique_ptr<views::Widget> widget);
+  views::Widget* widget() const { return widget_.get(); }
 
-  views::Widget* widget() const { return widget_; }
+  // Creates a widget containing |content|. The created widget will initially be
+  // shown.
+  std::unique_ptr<views::Widget> CreateWidgetWithContent(views::View* content);
 
   // Utility method to create a new |mojom::UserInfoPtr| instance.
   mojom::LoginUserInfoPtr CreateUser(const std::string& name) const;
@@ -46,8 +54,8 @@
  private:
   class WidgetDelegate;
 
-  views::Widget* widget_ = nullptr;
-  std::unique_ptr<WidgetDelegate> delegate_;
+  // The widget created using |ShowWidgetWithContent|.
+  std::unique_ptr<views::Widget> widget_;
 
   std::vector<mojom::LoginUserInfoPtr> users_;
 
diff --git a/ash/login/ui/login_user_view_unittest.cc b/ash/login/ui/login_user_view_unittest.cc
index 8d5e984..f066da6 100644
--- a/ash/login/ui/login_user_view_unittest.cc
+++ b/ash/login/ui/login_user_view_unittest.cc
@@ -45,7 +45,7 @@
     auto* root = new views::View();
     root->SetLayoutManager(new views::BoxLayout(views::BoxLayout::kHorizontal));
     root->AddChildView(container_);
-    ShowWidgetWithContent(root);
+    SetWidget(CreateWidgetWithContent(root));
   }
 
   int tap_count_ = 0;
@@ -229,4 +229,4 @@
   EXPECT_TRUE(two_test.is_opaque());
 }
 
-}  // namespace ash
\ No newline at end of file
+}  // namespace ash
diff --git a/ash/login/ui/note_action_launch_button_unittest.cc b/ash/login/ui/note_action_launch_button_unittest.cc
index 685d3f3..08ab207 100644
--- a/ash/login/ui/note_action_launch_button_unittest.cc
+++ b/ash/login/ui/note_action_launch_button_unittest.cc
@@ -19,6 +19,7 @@
 #include "ui/gfx/geometry/vector2d.h"
 #include "ui/views/layout/box_layout.h"
 #include "ui/views/view.h"
+#include "ui/views/widget/widget.h"
 
 namespace ash {
 
@@ -47,10 +48,6 @@
 
   TestTrayActionClient* tray_action_client() { return &tray_action_client_; }
 
-  void ShowNoteActionView(NoteActionLaunchButton* note_action) {
-    ShowWidgetWithContent(
-        login_layout_util::WrapViewForPreferredSize(note_action));
-  }
 
   void PerformClick(const gfx::Point& point) {
     ui::test::EventGenerator& generator = GetEventGenerator();
@@ -95,7 +92,8 @@
 TEST_F(NoteActionLaunchButtonTest, StateChanges) {
   auto* note_action_button = new NoteActionLaunchButton(
       mojom::TrayActionState::kAvailable, data_dispatcher());
-  ShowNoteActionView(note_action_button);
+  std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(
+      login_layout_util::WrapViewForPreferredSize(note_action_button));
   NoteActionLaunchButton::TestApi test_api(note_action_button);
 
   // In kAvailable state, the action button should be visible.
@@ -133,7 +131,8 @@
 TEST_F(NoteActionLaunchButtonTest, KeyboardTest) {
   auto* note_action_button = new NoteActionLaunchButton(
       mojom::TrayActionState::kAvailable, data_dispatcher());
-  ShowNoteActionView(note_action_button);
+  std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(
+      login_layout_util::WrapViewForPreferredSize(note_action_button));
   NoteActionLaunchButton::TestApi test_api(note_action_button);
 
   note_action_button->RequestFocus();
@@ -157,7 +156,8 @@
 TEST_F(NoteActionLaunchButtonTest, ClickTest) {
   auto* note_action_button = new NoteActionLaunchButton(
       mojom::TrayActionState::kAvailable, data_dispatcher());
-  ShowNoteActionView(note_action_button);
+  std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(
+      login_layout_util::WrapViewForPreferredSize(note_action_button));
 
   const gfx::Size action_size = note_action_button->GetPreferredSize();
   EXPECT_EQ(gfx::Size(kLargeButtonRadiusDp, kLargeButtonRadiusDp), action_size);
diff --git a/ash/public/interfaces/lock_screen.mojom b/ash/public/interfaces/lock_screen.mojom
index b13801f..5a5155b 100644
--- a/ash/public/interfaces/lock_screen.mojom
+++ b/ash/public/interfaces/lock_screen.mojom
@@ -60,6 +60,11 @@
   // |account_id|:   The account id of the user in the user pod.
   // |is_enabled|:   True if pin unlock is enabled.
   SetPinEnabledForUser(signin.mojom.AccountId account_id, bool is_enabled);
+
+  // Called when focus is reported to be leaving a lock screen app window.
+  // Requests focus to be handed off to the next suitable widget.
+  // |reverse|:   Whether the tab order is reversed.
+  HandleFocusLeavingLockScreenApps(bool reverse);
 };
 
 // Allows ash lock screen to control a client (e.g. Chrome browser). Requests
@@ -108,4 +113,12 @@
 
   // User with |account_id| has reached maximum incorrect password attempts.
   OnMaxIncorrectPasswordAttempted(signin.mojom.AccountId account_id);
+
+  // Should pass the focus to the active lock screen app window, if there is
+  // one. This is called when a lock screen app is reported to be active (using
+  // tray_action mojo interface), and is next in the tab order.
+  // |HandleFocusLeavingLockScreenApps| should be called to return focus to the
+  // lock screen.
+  // |reverse|:   Whether the tab order is reversed.
+  FocusLockScreenApps(bool reverse);
 };
diff --git a/chrome/browser/chromeos/login/lock/views_screen_locker.cc b/chrome/browser/chromeos/login/lock/views_screen_locker.cc
index 08d7329..fae53fb 100644
--- a/chrome/browser/chromeos/login/lock/views_screen_locker.cc
+++ b/chrome/browser/chromeos/login/lock/views_screen_locker.cc
@@ -9,6 +9,7 @@
 #include "base/i18n/time_formatting.h"
 #include "base/metrics/histogram_macros.h"
 #include "chrome/browser/browser_process.h"
+#include "chrome/browser/chromeos/lock_screen_apps/state_controller.h"
 #include "chrome/browser/chromeos/login/lock_screen_utils.h"
 #include "chrome/browser/chromeos/login/quick_unlock/quick_unlock_factory.h"
 #include "chrome/browser/chromeos/login/quick_unlock/quick_unlock_storage.h"
@@ -46,6 +47,8 @@
 }
 
 ViewsScreenLocker::~ViewsScreenLocker() {
+  if (lock_screen_apps::StateController::IsEnabled())
+    lock_screen_apps::StateController::Get()->SetFocusCyclerDelegate(nullptr);
   LockScreenClient::Get()->SetDelegate(nullptr);
 }
 
@@ -77,6 +80,8 @@
   UMA_HISTOGRAM_TIMES("LockScreen.LockReady",
                       base::TimeTicks::Now() - lock_time_);
   screen_locker_->ScreenLockReady();
+  if (lock_screen_apps::StateController::IsEnabled())
+    lock_screen_apps::StateController::Get()->SetFocusCyclerDelegate(this);
   OnAllowedInputMethodsChanged();
 }
 
@@ -201,6 +206,14 @@
   lock_screen_utils::EnforcePolicyInputMethods(std::string());
 }
 
+bool ViewsScreenLocker::HandleFocusLockScreenApps(bool reverse) {
+  if (lock_screen_app_focus_handler_.is_null())
+    return false;
+
+  lock_screen_app_focus_handler_.Run(reverse);
+  return true;
+}
+
 void ViewsScreenLocker::SuspendDone(const base::TimeDelta& sleep_duration) {
   for (user_manager::User* user :
        user_manager::UserManager::Get()->GetUnlockUsers()) {
@@ -208,6 +221,19 @@
   }
 }
 
+void ViewsScreenLocker::RegisterLockScreenAppFocusHandler(
+    const LockScreenAppFocusCallback& focus_handler) {
+  lock_screen_app_focus_handler_ = focus_handler;
+}
+
+void ViewsScreenLocker::UnregisterLockScreenAppFocusHandler() {
+  lock_screen_app_focus_handler_.Reset();
+}
+
+void ViewsScreenLocker::HandleLockScreenAppFocusOut(bool reverse) {
+  LockScreenClient::Get()->HandleFocusLeavingLockScreenApps(reverse);
+}
+
 void ViewsScreenLocker::UpdatePinKeyboardState(const AccountId& account_id) {
   quick_unlock::QuickUnlockStorage* quick_unlock_storage =
       quick_unlock::QuickUnlockFactory::GetForAccountId(account_id);
diff --git a/chrome/browser/chromeos/login/lock/views_screen_locker.h b/chrome/browser/chromeos/login/lock/views_screen_locker.h
index a7f692a..45b27eca 100644
--- a/chrome/browser/chromeos/login/lock/views_screen_locker.h
+++ b/chrome/browser/chromeos/login/lock/views_screen_locker.h
@@ -6,6 +6,7 @@
 #define CHROME_BROWSER_CHROMEOS_LOGIN_LOCK_VIEWS_SCREEN_LOCKER_H_
 
 #include "base/memory/weak_ptr.h"
+#include "chrome/browser/chromeos/lock_screen_apps/focus_cycler_delegate.h"
 #include "chrome/browser/chromeos/login/lock/screen_locker.h"
 #include "chrome/browser/chromeos/settings/cros_settings.h"
 #include "chrome/browser/ui/ash/lock_screen_client.h"
@@ -22,7 +23,8 @@
 // ash (views-based lockscreen).
 class ViewsScreenLocker : public LockScreenClient::Delegate,
                           public ScreenLocker::Delegate,
-                          public PowerManagerClient::Observer {
+                          public PowerManagerClient::Observer,
+                          public lock_screen_apps::FocusCyclerDelegate {
  public:
   explicit ViewsScreenLocker(ScreenLocker* screen_locker);
   ~ViewsScreenLocker() override;
@@ -54,10 +56,17 @@
   void HandleRecordClickOnLockIcon(const AccountId& account_id) override;
   void HandleOnFocusPod(const AccountId& account_id) override;
   void HandleOnNoPodFocused() override;
+  bool HandleFocusLockScreenApps(bool reverse) override;
 
   // PowerManagerClient::Observer:
   void SuspendDone(const base::TimeDelta& sleep_duration) override;
 
+  // lock_screen_apps::FocusCyclerDelegate:
+  void RegisterLockScreenAppFocusHandler(
+      const LockScreenAppFocusCallback& focus_handler) override;
+  void UnregisterLockScreenAppFocusHandler() override;
+  void HandleLockScreenAppFocusOut(bool reverse) override;
+
  private:
   void UpdatePinKeyboardState(const AccountId& account_id);
   void OnAllowedInputMethodsChanged();
@@ -81,6 +90,10 @@
 
   bool lock_screen_ready_ = false;
 
+  // Callback registered as a lock screen apps focus handler - it should be
+  // called to hand focus over to lock screen apps.
+  LockScreenAppFocusCallback lock_screen_app_focus_handler_;
+
   base::WeakPtrFactory<ViewsScreenLocker> weak_factory_;
 
   DISALLOW_COPY_AND_ASSIGN(ViewsScreenLocker);
diff --git a/chrome/browser/ui/ash/lock_screen_client.cc b/chrome/browser/ui/ash/lock_screen_client.cc
index af8804bb..02a8ad4 100644
--- a/chrome/browser/ui/ash/lock_screen_client.cc
+++ b/chrome/browser/ui/ash/lock_screen_client.cc
@@ -83,6 +83,14 @@
     delegate_->HandleOnNoPodFocused();
 }
 
+void LockScreenClient::FocusLockScreenApps(bool reverse) {
+  // If delegate is not set, or it fails to handle focus request, call
+  // |HandleFocusLeavingLockScreenApps| so the lock screen mojo service can
+  // give focus to the next window in the tab order.
+  if (!delegate_ || !delegate_->HandleFocusLockScreenApps(reverse))
+    HandleFocusLeavingLockScreenApps(reverse);
+}
+
 void LockScreenClient::LoadWallpaper(const AccountId& account_id) {
   chromeos::WallpaperManager::Get()->SetUserWallpaperDelayed(account_id);
 }
@@ -140,6 +148,10 @@
   lock_screen_->SetPinEnabledForUser(account_id, is_enabled);
 }
 
+void LockScreenClient::HandleFocusLeavingLockScreenApps(bool reverse) {
+  lock_screen_->HandleFocusLeavingLockScreenApps(reverse);
+}
+
 void LockScreenClient::SetDelegate(Delegate* delegate) {
   delegate_ = delegate;
 }
diff --git a/chrome/browser/ui/ash/lock_screen_client.h b/chrome/browser/ui/ash/lock_screen_client.h
index 52ade92..ca38dd6 100644
--- a/chrome/browser/ui/ash/lock_screen_client.h
+++ b/chrome/browser/ui/ash/lock_screen_client.h
@@ -34,6 +34,10 @@
     virtual void HandleRecordClickOnLockIcon(const AccountId& account_id) = 0;
     virtual void HandleOnFocusPod(const AccountId& account_id) = 0;
     virtual void HandleOnNoPodFocused() = 0;
+    // Handles request to focus a lock screen app window. Returns whether the
+    // focus has been handed over to a lock screen app. For example, this might
+    // fail if a hander for lock screen apps focus has not been set.
+    virtual bool HandleFocusLockScreenApps(bool reverse) = 0;
 
    private:
     DISALLOW_COPY_AND_ASSIGN(Delegate);
@@ -55,6 +59,7 @@
   void SignOutUser() override;
   void CancelAddUser() override;
   void OnMaxIncorrectPasswordAttempted(const AccountId& account_id) override;
+  void FocusLockScreenApps(bool reverse) override;
 
   // Wrappers around the mojom::LockScreen interface.
   void ShowLockScreen(ash::mojom::LockScreen::ShowLockScreenCallback on_shown);
@@ -72,6 +77,7 @@
   void LoadUsers(std::vector<ash::mojom::LoginUserInfoPtr> users_list,
                  bool show_guest);
   void SetPinEnabledForUser(const AccountId& account_id, bool is_enabled);
+  void HandleFocusLeavingLockScreenApps(bool reverse);
 
   void SetDelegate(Delegate* delegate);