[WebID] Mediated account selection UI - P5 - Add Header view

This CL is part of a series to implement prototype Android UI of the
account selection flow for WebID .

It adds the header to the view by adding it as the first item in sheet
data. It also adds relevant view and controller tests.

## Background on WebID mediation

Mediation is a formulation of WebID in which the user agent takes
responsibility in owning the identity exchange between RP and IDP.
It talks to the IDP directly to obtain user accounts and lets user
selects an account before completing the identity exchange between
RP and IDP.

[WebID](https://wicg.github.io/WebID/)
[WebID mediation](https://wicg.github.io/WebID/cookies.html#mediation)
[WebID detailed design](go/webid-detailed-design)

## Background on the CL series

This CL series is prototyping UI for this approach on Android based on
early exploration here: go/webid-mediated-ux

The specific use and structure of the code in this CL is documented
in two READMEs added as part of this CL:
chrome/browser/ui/android/webid/{README.md, internal/README.md}

The implementation here is inspired to some extent by touch_to_fill component
that is used for Chrome credential management on Android which creates
a bottom sheet.


Final screenshots for this prototype UI:
- https://storage.cloud.google.com/chromium-translation-screenshots/08019775363de674c8f549172ca75bf84c0a3461
- https://storage.cloud.google.com/chromium-translation-screenshots/c91fb7f6fa1a2d6827387313efb66f722f7383d4

[touch_to_fill component](https://chromium.googlesource.com/chromium/src/+/main/chrome/browser/touch_to_fill/android/README.md)


## This CL in the series

Here is where this CL sits in the series:

1. Add scaffold for C++ side
2. Create the Android UI component scaffold with an empty model.
3. Add VISIBLE, DISMISS_HANDLER properties to the model and create basic controller test.
4. Add view and view binder. Add view tests & integration test for these.
👉5. Add header to the view.
6. Add accounts list and "sign up" button to the view.


Bug: 1199088
Change-Id: I2b7246b47d47d362a34a390574d22296cbf018a8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2877959
Commit-Queue: Majid Valipour <majidvp@chromium.org>
Reviewed-by: Theresa  <twellington@chromium.org>
Cr-Commit-Position: refs/heads/master@{#890080}
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings.grd b/chrome/browser/ui/android/strings/android_chrome_strings.grd
index a52e3c6..109204c 100644
--- a/chrome/browser/ui/android/strings/android_chrome_strings.grd
+++ b/chrome/browser/ui/android/strings/android_chrome_strings.grd
@@ -4774,6 +4774,12 @@
       </message>
 
       <!-- WebID Account Selection strings -->
+      <message name="IDS_ACCOUNT_SELECTION_SHEET_TITLE_SINGLE" desc="Header for Account Selection sheet where users only have a single account to select." translateable="false">
+        Create a <ph name="SITE_NAME">%1$s<ex>airbnb.com</ex></ph> account
+      </message>
+      <message name="IDS_ACCOUNT_SELECTION_SHEET_TITLE" desc="Header for Account Selection sheet where users can pick an account to select." translateable="false">
+        Create a <ph name="SITE_NAME">%1$s<ex>airbnb.com</ex></ph> account…
+      </message>
       <message name="IDS_ACCOUNT_SELECTION_CONTENT_DESCRIPTION" desc="Accessibility string read when the Account Selection bottom sheet is opened. It describes the bottom sheet where a user can pick an account." translateable="false">
         List of accounts to be selected.
       </message>
diff --git a/chrome/browser/ui/android/webid/internal/BUILD.gn b/chrome/browser/ui/android/webid/internal/BUILD.gn
index ac75a43..3cf9fdd 100644
--- a/chrome/browser/ui/android/webid/internal/BUILD.gn
+++ b/chrome/browser/ui/android/webid/internal/BUILD.gn
@@ -18,6 +18,7 @@
     "//components/browser_ui/bottomsheet/android:java",
     "//components/browser_ui/widget/android:java",
     "//components/embedder_support/android:util_java",
+    "//components/url_formatter/android:url_formatter_java",
     "//third_party/androidx:androidx_annotation_annotation_java",
     "//third_party/androidx:androidx_recyclerview_recyclerview_java",
     "//ui/android:ui_java",
@@ -30,6 +31,7 @@
     "java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionCoordinator.java",
     "java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionMediator.java",
     "java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionProperties.java",
+    "java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionViewBinder.java",
   ]
 
   resources_package = "org.chromium.chrome.browser.ui.android.webid"
@@ -43,6 +45,7 @@
     "//ui/android:ui_java_resources",
   ]
   sources = [
+    "java/res/layout/account_selection_header_item.xml",
     "java/res/layout/account_selection_sheet.xml",
     "java/res/values/dimens.xml",
   ]
@@ -64,6 +67,7 @@
     "//chrome/browser/ui/android/webid:public_java",
     "//chrome/browser/ui/android/webid/internal:java",
     "//components/browser_ui/bottomsheet/android:java",
+    "//components/url_formatter/android:url_formatter_java",
     "//third_party/android_deps:robolectric_all_java",
     "//third_party/androidx:androidx_annotation_annotation_java",
     "//third_party/hamcrest:hamcrest_java",
@@ -76,7 +80,10 @@
 android_library("javatests") {
   testonly = true
 
-  sources = [ "java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionIntegrationTest.java" ]
+  sources = [
+    "java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionIntegrationTest.java",
+    "java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionViewTest.java",
+  ]
 
   deps = [
     ":java",
@@ -99,6 +106,7 @@
     "//third_party/junit",
     "//third_party/mockito:mockito_java",
     "//ui/android:ui_full_java",
+    "//ui/android:ui_java_test_support",
     "//ui/android:ui_utils_java",
   ]
 }
diff --git a/chrome/browser/ui/android/webid/internal/README.md b/chrome/browser/ui/android/webid/internal/README.md
index e513796..a012b9d 100644
--- a/chrome/browser/ui/android/webid/internal/README.md
+++ b/chrome/browser/ui/android/webid/internal/README.md
@@ -55,10 +55,14 @@
 
 We use a simple `LinearLayout` as the top-level view for this component which
 contains the list view for sheet items. This view is them displayed inside the
-bottom sheet via `AccountSelectionBottomSheetContent`.
+bottom sheet via `AccountSelectionBottomSheetContent`. The rest of the logic is
+split in two parts:
 
-`AccountSelectionBottomSheetContent` is a simple container that implements
-`BottomSheetContent` interface and facilitates display of our view via the
-`BottomSheetController`. The bottom sheet controller instance itself is
-controlled by the mediator to create and modify the bottom sheet where accounts
-are displayed.
+ * **AccountSelectionViewBinder** which maps model changes to the view. This is
+   mainly used by `SimpleRecyclerViewAdapter` and is responsible to bind changes
+   to the items in the model list to the RecyclerView inside the bottom sheet.
+ * **AccountSelectionBottomSheetContent** This is a simple container that
+   implements `BottomSheetContent` interface and facilitates display of our view
+   inside the `BottomSheetController`. The bottom sheet controller instance
+   itself is controlled by the mediator to create and modify the bottom sheet
+   where accounts are displayed.
diff --git a/chrome/browser/ui/android/webid/internal/java/res/layout/account_selection_header_item.xml b/chrome/browser/ui/android/webid/internal/java/res/layout/account_selection_header_item.xml
new file mode 100644
index 0000000..ee8256a9e
--- /dev/null
+++ b/chrome/browser/ui/android/webid/internal/java/res/layout/account_selection_header_item.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2021 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. -->
+
+<!-- Please update R.dimens.account_selection_sheet_height_single_account
+     when modifying the margins or text sizes. -->
+<org.chromium.ui.widget.TextViewWithLeading
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/account_selection_sheet_title"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center_horizontal"
+    android:textAppearance="@style/TextAppearance.Headline.Primary"
+    android:layout_marginBottom="16dp"/>
diff --git a/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionControllerTest.java b/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionControllerTest.java
index 45c888e1..9e0ef2c 100644
--- a/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionControllerTest.java
+++ b/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionControllerTest.java
@@ -8,9 +8,15 @@
 import static org.junit.Assert.assertThat;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import static org.chromium.chrome.browser.ui.android.webid.AccountSelectionProperties.HeaderProperties.FORMATTED_URL;
+import static org.chromium.chrome.browser.ui.android.webid.AccountSelectionProperties.HeaderProperties.SINGLE_ACCOUNT;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -21,8 +27,12 @@
 
 import org.chromium.base.test.BaseRobolectricTestRunner;
 import org.chromium.base.test.util.JniMocker;
+import org.chromium.chrome.browser.ui.android.webid.AccountSelectionProperties.ItemType;
 import org.chromium.chrome.browser.ui.android.webid.data.Account;
 import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
+import org.chromium.components.url_formatter.SchemeDisplay;
+import org.chromium.components.url_formatter.UrlFormatter;
+import org.chromium.components.url_formatter.UrlFormatterJni;
 import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
 
 import java.util.Arrays;
@@ -39,16 +49,18 @@
     private static final String TEST_PROFILE_PIC = "https://www.example.xyz/profile/2";
 
     private static final Account ANA =
-            new Account("Ana", "S3cr3t", "Ana Doe", "Ana", TEST_PROFILE_PIC, TEST_URL);
+            new Account("Ana", "S3cr3t", "Ana Doe", "Ana", TEST_PROFILE_PIC, "https://m.a.xyz/");
     private static final Account BOB =
             new Account("Bob", "*****", "Bob", "", TEST_PROFILE_PIC, TEST_SUBDOMAIN_URL);
+    private static final Account CARL =
+            new Account("Carl", "G3h3!m", "Carl Test", ":)", TEST_PROFILE_PIC, TEST_URL);
 
     @Rule
     public JniMocker mJniMocker = new JniMocker();
-
+    @Mock
+    private UrlFormatter.Natives mUrlFormatterJniMock;
     @Mock
     private AccountSelectionComponent.Delegate mMockDelegate;
-
     @Mock
     private BottomSheetController mMockBottomSheetController;
 
@@ -58,15 +70,44 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
+        mJniMocker.mock(UrlFormatterJni.TEST_HOOKS, mUrlFormatterJniMock);
+        when(mUrlFormatterJniMock.formatUrlForDisplayOmitScheme(anyString()))
+                .then(inv -> format(inv.getArgument(0)));
+        when(mUrlFormatterJniMock.formatStringUrlForSecurityDisplay(
+                     anyString(), eq(SchemeDisplay.OMIT_HTTP_AND_HTTPS)))
+                .then(inv -> formatForSecurityDisplay(inv.getArgument(0)));
+
         mMediator = new AccountSelectionMediator(
                 mMockDelegate, mSheetItems, mMockBottomSheetController, null);
     }
 
     @Test
+    public void testShowAccountsCreatesHeader() {
+        mMediator.showAccounts(TEST_URL, Arrays.asList(ANA, BOB));
+        assertThat("Incorrect header type", mSheetItems.get(0).type, is(ItemType.HEADER));
+        assertThat("Incorrect header multiple accounts",
+                mSheetItems.get(0).model.get(SINGLE_ACCOUNT), is(false));
+        assertThat("Incorrect header url", mSheetItems.get(0).model.get(FORMATTED_URL),
+                is(formatForSecurityDisplay(TEST_URL)));
+    }
+
+    @Test
+    public void testShowAccountWithSingleEntryCreatesHeader() {
+        mMediator.showAccounts(TEST_URL, Arrays.asList(ANA));
+        assertThat("Incorrect header type", mSheetItems.get(0).type, is(ItemType.HEADER));
+        assertThat("Incorrect header single account", mSheetItems.get(0).model.get(SINGLE_ACCOUNT),
+                is(true));
+        assertThat("Incorrect header url", mSheetItems.get(0).model.get(FORMATTED_URL),
+                is(formatForSecurityDisplay(TEST_URL)));
+    }
+
+    @Test
     public void testShowAccountsSetsVisibile() {
         when(mMockBottomSheetController.requestShowContent(any(), anyBoolean())).thenReturn(true);
-        mMediator.showAccounts(TEST_URL, Arrays.asList(ANA, BOB));
-        assertThat(mMediator.isVisible(), is(true));
+        mMediator.showAccounts(TEST_URL, Arrays.asList(ANA, CARL, BOB));
+        verify(mMockBottomSheetController, times(1)).requestShowContent(eq(null), eq(true));
+
+        assertThat("Incorrect visibility", mMediator.isVisible(), is(true));
     }
 
     @Test
@@ -75,7 +116,7 @@
         mMediator.showAccounts(TEST_URL, Arrays.asList(ANA, BOB));
         mMediator.onDismissed(BottomSheetController.StateChangeReason.BACK_PRESS);
         verify(mMockDelegate).onDismissed();
-        assertThat(mMediator.isVisible(), is(false));
+        assertThat("Incorrect visibility", mMediator.isVisible(), is(false));
     }
 
     @Test
@@ -84,6 +125,27 @@
         mMediator.showAccounts(TEST_URL, Arrays.asList(ANA, BOB));
         mMediator.onAccountSelected(ANA);
         verify(mMockDelegate).onAccountSelected(ANA);
-        assertThat(mMediator.isVisible(), is(false));
+        assertThat("Incorrect visibility", mMediator.isVisible(), is(false));
+    }
+
+    /**
+     * Helper to verify formatted URLs. The real implementation calls {@link UrlFormatter}. It's not
+     * useful to actually reimplement the formatter, so just modify the string in a trivial way.
+     * @param originUrl A URL {@link String} to "format".
+     * @return A "formatted" URL {@link String}.
+     */
+    private static String format(String originUrl) {
+        return "formatted_" + originUrl + "_formatted";
+    }
+
+    /**
+     * Helper to verify URLs formatted for security display. The real implementation calls
+     * {@link UrlFormatter}. It's not useful to actually reimplement the formatter, so just
+     * modify the string in a trivial way.
+     * @param originUrl A URL {@link String} to "format".
+     * @return A "formatted" URL {@link String}.
+     */
+    private static String formatForSecurityDisplay(String originUrl) {
+        return "formatted_for_security_" + originUrl + "_formatted_for_security";
     }
 }
diff --git a/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionCoordinator.java b/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionCoordinator.java
index dfe085a..30259d14 100644
--- a/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionCoordinator.java
+++ b/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionCoordinator.java
@@ -8,6 +8,7 @@
 import android.content.res.Resources;
 import android.view.LayoutInflater;
 import android.view.View;
+import android.view.ViewGroup;
 import android.widget.LinearLayout;
 
 import androidx.annotation.Px;
@@ -62,11 +63,19 @@
 
         // Setup the recycler view to be updated as we update the sheet items.
         SimpleRecyclerViewAdapter adapter = new SimpleRecyclerViewAdapter(sheetItems);
+        adapter.registerType(AccountSelectionProperties.ItemType.HEADER,
+                AccountSelectionCoordinator::buildHeaderView,
+                AccountSelectionViewBinder::bindHeaderView);
         sheetItemListView.setAdapter(adapter);
 
         return contentView;
     }
 
+    static View buildHeaderView(ViewGroup parent) {
+        return LayoutInflater.from(parent.getContext())
+                .inflate(R.layout.account_selection_header_item, parent, false);
+    }
+
     @Override
     public void showAccounts(String url, List<Account> accounts) {
         mMediator.showAccounts(url, accounts);
diff --git a/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionIntegrationTest.java b/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionIntegrationTest.java
index c3d0b81..044a3c2 100644
--- a/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionIntegrationTest.java
+++ b/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionIntegrationTest.java
@@ -31,6 +31,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import org.chromium.base.test.util.Batch;
 import org.chromium.base.test.util.CommandLineFlags;
 import org.chromium.base.test.util.CriteriaHelper;
 import org.chromium.base.test.util.ScalableTimeout;
@@ -50,6 +51,7 @@
  * Selection API end up rendering a View.
  */
 @RunWith(ChromeJUnit4ClassRunner.class)
+@Batch(Batch.PER_CLASS)
 @CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
 public class AccountSelectionIntegrationTest {
     private static final String EXAMPLE_URL = "https://www.example.xyz";
@@ -61,7 +63,7 @@
     private static final Account BOB =
             new Account("Bob", "*****", "Bob", "", TEST_PROFILE_PIC, MOBILE_URL);
 
-    private final AccountSelectionComponent mAccountSelection = new AccountSelectionCoordinator();
+    private AccountSelectionComponent mAccountSelection;
 
     @Mock
     private AccountSelectionComponent.Delegate mMockBridge;
@@ -71,12 +73,10 @@
 
     private BottomSheetController mBottomSheetController;
 
-    public AccountSelectionIntegrationTest() {
-        MockitoAnnotations.initMocks(this);
-    }
-
     @Before
     public void setUp() throws InterruptedException {
+        MockitoAnnotations.initMocks(this);
+        mAccountSelection = new AccountSelectionCoordinator();
         mActivityTestRule.startMainActivityOnBlankPage();
         runOnUiThreadBlocking(() -> {
             mBottomSheetController = BottomSheetControllerProvider.from(
diff --git a/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionMediator.java b/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionMediator.java
index b3b45cc..4363f77 100644
--- a/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionMediator.java
+++ b/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionMediator.java
@@ -4,15 +4,23 @@
 
 package org.chromium.chrome.browser.ui.android.webid;
 
+import static org.chromium.chrome.browser.ui.android.webid.AccountSelectionProperties.HeaderProperties.FORMATTED_URL;
+import static org.chromium.chrome.browser.ui.android.webid.AccountSelectionProperties.HeaderProperties.SINGLE_ACCOUNT;
+
 import androidx.annotation.VisibleForTesting;
 
+import org.chromium.chrome.browser.ui.android.webid.AccountSelectionProperties.HeaderProperties;
 import org.chromium.chrome.browser.ui.android.webid.data.Account;
 import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
 import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
 import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.StateChangeReason;
 import org.chromium.components.browser_ui.bottomsheet.BottomSheetObserver;
 import org.chromium.components.browser_ui.bottomsheet.EmptyBottomSheetObserver;
+import org.chromium.components.url_formatter.SchemeDisplay;
+import org.chromium.components.url_formatter.UrlFormatter;
+import org.chromium.ui.modelutil.MVCListAdapter.ListItem;
 import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
+import org.chromium.ui.modelutil.PropertyModel;
 
 import java.util.List;
 
@@ -54,7 +62,18 @@
     }
 
     void showAccounts(String url, List<Account> accounts) {
-        // TODO (majidvp): Update mSheetItems and show the view.
+        mSheetItems.clear();
+
+        // We remove the HTTPS from URL since it is the only protocol that is
+        // allowed with WebID.
+        mSheetItems.add(new ListItem(AccountSelectionProperties.ItemType.HEADER,
+                new PropertyModel.Builder(HeaderProperties.ALL_KEYS)
+                        .with(SINGLE_ACCOUNT, accounts.size() == 1)
+                        .with(FORMATTED_URL,
+                                UrlFormatter.formatUrlForSecurityDisplay(
+                                        url, SchemeDisplay.OMIT_HTTP_AND_HTTPS))
+                        .build()));
+        // TODO(majidvp): Add accounts to the sheet items.
         showContent();
     }
 
diff --git a/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionProperties.java b/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionProperties.java
index bd282bd..c401e5f 100644
--- a/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionProperties.java
+++ b/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionProperties.java
@@ -4,24 +4,40 @@
 
 package org.chromium.chrome.browser.ui.android.webid;
 
-import org.chromium.base.Callback;
+import androidx.annotation.IntDef;
+
+import org.chromium.ui.modelutil.PropertyKey;
 import org.chromium.ui.modelutil.PropertyModel;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
- * Properties defined here reflect the visible state of the AccountSelection-components.
+ * Properties defined here reflect the state of the AccountSelection-components.
  */
 class AccountSelectionProperties {
-    static final PropertyModel.WritableBooleanPropertyKey VISIBLE =
-            new PropertyModel.WritableBooleanPropertyKey("visible");
+    /**
+     * Properties defined here reflect the state of the header in the AccountSelection
+     * sheet.
+     */
+    static class HeaderProperties {
+        static final PropertyModel.ReadableBooleanPropertyKey SINGLE_ACCOUNT =
+                new PropertyModel.ReadableBooleanPropertyKey("single_account");
+        static final PropertyModel.ReadableObjectPropertyKey<String> FORMATTED_URL =
+                new PropertyModel.ReadableObjectPropertyKey<>("formatted_url");
 
-    static final PropertyModel.ReadableObjectPropertyKey<Callback<Integer>> DISMISS_HANDLER =
-            new PropertyModel.ReadableObjectPropertyKey<>("dismiss_handler");
+        static final PropertyKey[] ALL_KEYS = {SINGLE_ACCOUNT, FORMATTED_URL};
 
-    static PropertyModel createDefaultModel(Callback<Integer> handler) {
-        return new PropertyModel.Builder(VISIBLE, DISMISS_HANDLER)
-                .with(VISIBLE, false)
-                .with(DISMISS_HANDLER, handler)
-                .build();
+        private HeaderProperties() {}
+    }
+
+    @IntDef({ItemType.HEADER})
+    @Retention(RetentionPolicy.SOURCE)
+    @interface ItemType {
+        /**
+         * The header at the top of the accounts sheet.
+         */
+        int HEADER = 1;
     }
 
     private AccountSelectionProperties() {}
diff --git a/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionViewBinder.java b/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionViewBinder.java
new file mode 100644
index 0000000..a88f71c4
--- /dev/null
+++ b/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionViewBinder.java
@@ -0,0 +1,49 @@
+// Copyright 2021 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.
+
+package org.chromium.chrome.browser.ui.android.webid;
+
+import static org.chromium.chrome.browser.ui.android.webid.AccountSelectionProperties.HeaderProperties.FORMATTED_URL;
+import static org.chromium.chrome.browser.ui.android.webid.AccountSelectionProperties.HeaderProperties.SINGLE_ACCOUNT;
+
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.StringRes;
+
+import org.chromium.ui.modelutil.PropertyKey;
+import org.chromium.ui.modelutil.PropertyModel;
+
+/**
+ * Provides functions that map {@link AccountSelectionProperties} changes in a {@link PropertyModel}
+ * to the suitable method in {@link AccountSelectionView}.
+ */
+class AccountSelectionViewBinder {
+    /**
+     * Called whenever a property in the given model changes. It updates the given view accordingly.
+     * @param model The observed {@link PropertyModel}. Its data need to be reflected in the view.
+     * @param view The {@link View} of the header to update.
+     * @param key The {@link PropertyKey} which changed.
+     */
+    static void bindHeaderView(PropertyModel model, View view, PropertyKey key) {
+        if (key == SINGLE_ACCOUNT || key == FORMATTED_URL) {
+            TextView sheetTitleText = view.findViewById(R.id.account_selection_sheet_title);
+            @StringRes
+            int titleStringId;
+            if (model.get(SINGLE_ACCOUNT)) {
+                titleStringId = R.string.account_selection_sheet_title_single;
+            } else {
+                titleStringId = R.string.account_selection_sheet_title;
+            }
+
+            String title = String.format(
+                    view.getContext().getString(titleStringId), model.get(FORMATTED_URL));
+            sheetTitleText.setText(title);
+        } else {
+            assert false : "Unhandled update to property:" + key;
+        }
+    }
+
+    private AccountSelectionViewBinder() {}
+}
diff --git a/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionViewTest.java b/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionViewTest.java
new file mode 100644
index 0000000..48d893b
--- /dev/null
+++ b/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionViewTest.java
@@ -0,0 +1,94 @@
+// Copyright 2021 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.
+
+package org.chromium.chrome.browser.ui.android.webid;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+
+import static org.chromium.base.test.util.CriteriaHelper.pollUiThread;
+import static org.chromium.chrome.browser.ui.android.webid.AccountSelectionProperties.HeaderProperties.FORMATTED_URL;
+import static org.chromium.chrome.browser.ui.android.webid.AccountSelectionProperties.HeaderProperties.SINGLE_ACCOUNT;
+
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.test.filters.MediumTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.chromium.base.test.BaseActivityTestRule;
+import org.chromium.base.test.util.Batch;
+import org.chromium.chrome.browser.ui.android.webid.AccountSelectionProperties.HeaderProperties;
+import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
+import org.chromium.content_public.browser.test.util.TestThreadUtils;
+import org.chromium.ui.modelutil.MVCListAdapter;
+import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
+import org.chromium.ui.modelutil.PropertyModel;
+import org.chromium.ui.test.util.DummyUiActivity;
+
+/**
+ * View tests for the Account Selection component ensure that model changes are reflected in the
+ * sheet.
+ */
+@RunWith(ChromeJUnit4ClassRunner.class)
+@Batch(Batch.UNIT_TESTS)
+public class AccountSelectionViewTest {
+    private DummyUiActivity mActivity;
+    private ModelList mSheetItems;
+    private View mContentView;
+    @Rule
+    public BaseActivityTestRule<DummyUiActivity> mActivityTestRule =
+            new BaseActivityTestRule<>(DummyUiActivity.class);
+
+    @Before
+    public void setUp() throws Exception {
+        mActivityTestRule.launchActivity(null);
+        mActivity = mActivityTestRule.getActivity();
+
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
+            mSheetItems = new ModelList();
+            mContentView = AccountSelectionCoordinator.setupContentView(mActivity, mSheetItems);
+            mActivity.setContentView(mContentView);
+        });
+    }
+
+    @Test
+    @MediumTest
+    public void testSingleAccountTitleDisplayed() {
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
+            mSheetItems.add(new MVCListAdapter.ListItem(AccountSelectionProperties.ItemType.HEADER,
+                    new PropertyModel.Builder(HeaderProperties.ALL_KEYS)
+                            .with(SINGLE_ACCOUNT, true)
+                            .with(FORMATTED_URL, "www.example.org")
+                            .build()));
+        });
+        pollUiThread(() -> mContentView.getVisibility() == View.VISIBLE);
+        TextView title = mContentView.findViewById(R.id.account_selection_sheet_title);
+
+        assertThat("Incorrect title", title.getText(),
+                is(mActivity.getString(
+                        R.string.account_selection_sheet_title_single, "www.example.org")));
+    }
+
+    @Test
+    @MediumTest
+    public void testMultiAccountTitleDisplayed() {
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
+            mSheetItems.add(new MVCListAdapter.ListItem(AccountSelectionProperties.ItemType.HEADER,
+                    new PropertyModel.Builder(HeaderProperties.ALL_KEYS)
+                            .with(SINGLE_ACCOUNT, false)
+                            .with(FORMATTED_URL, "www.example.org")
+                            .build()));
+        });
+        pollUiThread(() -> mContentView.getVisibility() == View.VISIBLE);
+        TextView title = mContentView.findViewById(R.id.account_selection_sheet_title);
+
+        assertThat("Incorrect title", title.getText(),
+                is(mActivity.getString(R.string.account_selection_sheet_title, "www.example.org")));
+    }
+}