Commit 2956d18f authored by Alice Wang's avatar Alice Wang Committed by Commit Bot

[Signin] Add |Continue as| button to account picker web sign-in flow

This CL adds the |Continue as| to account picker bottom sheet in the
web sign-in flow and the collapsing/expanding mechanism to the bottom
sheet. Prior to this CL, the bottom sheet shows a straightforward
recycler view of the account list as in https://crbug.com/1081253#c1.

Screenshot: https://crbug.com/1093449#c2, https://crbug.com/1093795
Bug: 1093449
Change-Id: Ie4460523ea794d48efff384ba670c23348ae5c46
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2238192
Commit-Queue: Alice Wang <aliceywang@chromium.org>
Reviewed-by: default avatarBoris Sazonov <bsazonov@chromium.org>
Cr-Commit-Position: refs/heads/master@{#783892}
parent ae31d1da
......@@ -1423,6 +1423,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/signin/SyncPromoView.java",
"java/src/org/chromium/chrome/browser/signin/UnifiedConsentServiceBridge.java",
"java/src/org/chromium/chrome/browser/signin/account_picker/AccountPickerBottomSheetCoordinator.java",
"java/src/org/chromium/chrome/browser/signin/account_picker/AccountPickerBottomSheetMediator.java",
"java/src/org/chromium/chrome/browser/signin/account_picker/AccountPickerBottomSheetProperties.java",
"java/src/org/chromium/chrome/browser/signin/account_picker/AccountPickerBottomSheetView.java",
"java/src/org/chromium/chrome/browser/signin/account_picker/AccountPickerBottomSheetViewBinder.java",
......
......@@ -39,9 +39,30 @@
style="@style/HorizontalDivider" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/account_picker_item"
android:id="@+id/account_picker_account_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:visibility="gone"
tools:listitem="@layout/account_chooser_dialog_item" />
<include
android:id="@+id/account_picker_selected_account"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
layout="@layout/account_picker_row" />
<org.chromium.ui.widget.ButtonCompat
android:id="@+id/account_picker_continue_as_button"
style="@style/FilledButton.Flat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="20dp"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/signin_promo_continue_as" />
</LinearLayout>
\ No newline at end of file
......@@ -11,13 +11,13 @@ import androidx.annotation.MainThread;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetObserver;
import org.chromium.components.browser_ui.bottomsheet.EmptyBottomSheetObserver;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
/**
* Coordinator of the account picker bottom sheet used in web signin flow.
*/
public class AccountPickerBottomSheetCoordinator {
private final AccountPickerBottomSheetMediator mAccountPickerBottomSheetMediator;
private final AccountPickerCoordinator mAccountPickerCoordinator;
private final BottomSheetController mBottomSheetController;
private final BottomSheetObserver mBottomSheetObserver = new EmptyBottomSheetObserver() {
......@@ -32,18 +32,20 @@ public class AccountPickerBottomSheetCoordinator {
/**
* Constructs the AccountPickerBottomSheetCoordinator and shows the
* bottomsheet on the screen.
* bottom sheet on the screen.
*/
@MainThread
public AccountPickerBottomSheetCoordinator(Context context,
BottomSheetController bottomSheetController,
AccountPickerCoordinator.Listener accountPickerListener) {
AccountPickerDelegate accountPickerDelegate) {
AccountPickerBottomSheetView view = new AccountPickerBottomSheetView(context);
mAccountPickerBottomSheetMediator =
new AccountPickerBottomSheetMediator(context, accountPickerDelegate);
mAccountPickerCoordinator = new AccountPickerCoordinator(
view.getAccountPickerItemView(), accountPickerListener, null);
view.getAccountListView(), mAccountPickerBottomSheetMediator, null);
mBottomSheetController = bottomSheetController;
PropertyModel model = AccountPickerBottomSheetProperties.createModel();
PropertyModelChangeProcessor.create(model, view, AccountPickerBottomSheetViewBinder::bind);
PropertyModelChangeProcessor.create(mAccountPickerBottomSheetMediator.getModel(), view,
AccountPickerBottomSheetViewBinder::bind);
mBottomSheetController.addObserver(mBottomSheetObserver);
mBottomSheetController.requestShowContent(view, true);
}
......@@ -54,6 +56,8 @@ public class AccountPickerBottomSheetCoordinator {
@MainThread
private void destroy() {
mAccountPickerCoordinator.destroy();
mAccountPickerBottomSheetMediator.destroy();
mBottomSheetController.removeObserver(mBottomSheetObserver);
}
}
// Copyright 2020 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.signin.account_picker;
import android.accounts.Account;
import android.content.Context;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.signin.ProfileDataCache;
import org.chromium.components.signin.AccountManagerFacadeProvider;
import org.chromium.ui.modelutil.PropertyModel;
import java.util.Collections;
import java.util.List;
/**
* Mediator of the account picker bottom sheet in web sign-in flow.
*/
class AccountPickerBottomSheetMediator implements AccountPickerCoordinator.Listener {
private final AccountPickerDelegate mAccountPickerDelegate;
private final ProfileDataCache mProfileDataCache;
private final PropertyModel mModel;
private final ProfileDataCache.Observer mProfileDataSourceObserver =
this::updateSelectedAccountData;
private @Nullable String mSelectedAccountName = null;
AccountPickerBottomSheetMediator(Context context, AccountPickerDelegate accountPickerDelegate) {
mAccountPickerDelegate = accountPickerDelegate;
mProfileDataCache = new ProfileDataCache(
context, context.getResources().getDimensionPixelSize(R.dimen.user_picture_size));
mModel = AccountPickerBottomSheetProperties.createModel(
this::onSelectedAccountClicked, this::onContinueAsClicked);
mProfileDataCache.addObserver(mProfileDataSourceObserver);
// TODO(https://crbug.com/1096977): Add an observer to listen to accounts
// change in AccountManagerFacade, in case from zero to more accounts or
// the selected account disappeared.
List<Account> accounts = AccountManagerFacadeProvider.getInstance().tryGetGoogleAccounts();
if (!accounts.isEmpty()) {
setSelectedAccountName(accounts.get(0).name);
}
}
/**
* Notifies that the user has selected an account.
*
* @param accountName The email of the selected account.
* @param isDefaultAccount Whether the selected account is the first in the account list.
*/
@Override
public void onAccountSelected(String accountName, boolean isDefaultAccount) {
// Click on one account in the account list when the account list is expanded
// will collapse it to the selected account
mModel.set(AccountPickerBottomSheetProperties.IS_ACCOUNT_LIST_EXPANDED, false);
setSelectedAccountName(accountName);
}
/**
* Notifies when the user clicked the "add account" button.
*/
@Override
public void addAccount() {
mAccountPickerDelegate.addAccount();
}
PropertyModel getModel() {
return mModel;
}
void destroy() {
mProfileDataCache.removeObserver(mProfileDataSourceObserver);
}
private void setSelectedAccountName(String accountName) {
mSelectedAccountName = accountName;
mProfileDataCache.update(Collections.singletonList(mSelectedAccountName));
updateSelectedAccountData(mSelectedAccountName);
}
/**
* Implements {@link ProfileDataCache.Observer}.
*/
private void updateSelectedAccountData(String accountName) {
if (TextUtils.equals(mSelectedAccountName, accountName)) {
mModel.set(AccountPickerBottomSheetProperties.SELECTED_ACCOUNT_DATA,
mProfileDataCache.getProfileDataOrDefault(accountName));
}
}
/**
* Callback for the PropertyKey
* {@link AccountPickerBottomSheetProperties#ON_SELECTED_ACCOUNT_CLICKED}.
*/
private void onSelectedAccountClicked() {
// Clicking on the selected account when the account list is collapsed will expand the
// account list and make the account list visible
mModel.set(AccountPickerBottomSheetProperties.IS_ACCOUNT_LIST_EXPANDED, true);
}
/**
* Callback for the PropertyKey
* {@link AccountPickerBottomSheetProperties#ON_CONTINUE_AS_CLICKED}.
*/
private void onContinueAsClicked() {
if (mSelectedAccountName == null) {
addAccount();
} else {
mAccountPickerDelegate.signIn(mSelectedAccountName);
}
}
}
......@@ -4,18 +4,46 @@
package org.chromium.chrome.browser.signin.account_picker;
import org.chromium.chrome.browser.signin.DisplayableProfileData;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModel.ReadableObjectPropertyKey;
import org.chromium.ui.modelutil.PropertyModel.WritableBooleanPropertyKey;
import org.chromium.ui.modelutil.PropertyModel.WritableObjectPropertyKey;
/**
* Properties of account picker bottomsheet.
*
* TODO(http://crbug.com/1081253): Implement the bottomsheet properties.
* Properties of account picker bottom sheet.
*/
class AccountPickerBottomSheetProperties {
static final PropertyKey[] ALL_KEYS = new PropertyKey[] {};
// PropertyKeys for the selected account view when the account list is collapsed.
// The selected account view is replaced by account list view when the
// account list is expanded.
static final ReadableObjectPropertyKey<Runnable> ON_SELECTED_ACCOUNT_CLICKED =
new ReadableObjectPropertyKey<>("on_selected_account_clicked");
static final WritableObjectPropertyKey<DisplayableProfileData> SELECTED_ACCOUNT_DATA =
new WritableObjectPropertyKey<>("selected_account_data");
static PropertyModel createModel() {
return new PropertyModel.Builder(ALL_KEYS).build();
// PropertyKey for the button |Continue as ...|
// The button is visible during all the lifecycle of the bottom sheet
static final ReadableObjectPropertyKey<Runnable> ON_CONTINUE_AS_CLICKED =
new ReadableObjectPropertyKey<>("on_continue_as_clicked");
// PropertyKey indicates if the account list is expanded
static final WritableBooleanPropertyKey IS_ACCOUNT_LIST_EXPANDED =
new WritableBooleanPropertyKey("is_account_list_expanded");
static final PropertyKey[] ALL_KEYS = new PropertyKey[] {ON_SELECTED_ACCOUNT_CLICKED,
SELECTED_ACCOUNT_DATA, ON_CONTINUE_AS_CLICKED, IS_ACCOUNT_LIST_EXPANDED};
static PropertyModel createModel(
Runnable onSelectedAccountClicked, Runnable onContinueAsClicked) {
return new PropertyModel.Builder(ALL_KEYS)
.with(ON_SELECTED_ACCOUNT_CLICKED, onSelectedAccountClicked)
.with(SELECTED_ACCOUNT_DATA, null)
.with(ON_CONTINUE_AS_CLICKED, onContinueAsClicked)
.with(IS_ACCOUNT_LIST_EXPANDED, false)
.build();
}
private AccountPickerBottomSheetProperties() {}
}
......@@ -7,33 +7,103 @@ package org.chromium.chrome.browser.signin.account_picker;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.signin.DisplayableProfileData;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
import org.chromium.ui.widget.ButtonCompat;
/**
* This class is the AccountPickerBottomsheet view for the web sign-in flow.
*
* The bottom sheet shows a single account with a |Continue as ...| button by default, clicking
* on the account will expand the bottom sheet to an account list together with other sign-in
* options like "Add account" and "Go incognito mode".
*/
class AccountPickerBottomSheetView implements BottomSheetContent {
private final Context mContext;
private final View mContentView;
private final RecyclerView mAccountPickerItemView;
private final RecyclerView mAccountListView;
private final View mSelectedAccountView;
private final ButtonCompat mContinueAsButton;
AccountPickerBottomSheetView(Context context) {
mContext = context;
mContentView = LayoutInflater.from(mContext).inflate(
R.layout.account_picker_bottom_sheet_view, null);
mAccountPickerItemView = mContentView.findViewById(R.id.account_picker_item);
mAccountPickerItemView.setLayoutManager(new LinearLayoutManager(
mAccountPickerItemView.getContext(), LinearLayoutManager.VERTICAL, false));
mAccountListView = mContentView.findViewById(R.id.account_picker_account_list);
mAccountListView.setLayoutManager(new LinearLayoutManager(
mAccountListView.getContext(), LinearLayoutManager.VERTICAL, false));
mSelectedAccountView = mContentView.findViewById(R.id.account_picker_selected_account);
mContinueAsButton = mContentView.findViewById(R.id.account_picker_continue_as_button);
}
RecyclerView getAccountPickerItemView() {
return mAccountPickerItemView;
/**
* The account list view is visible when the account list is expanded.
*/
RecyclerView getAccountListView() {
return mAccountListView;
}
/**
* The selected account is visible when the account list is collapsed.
*/
View getSelectedAccountView() {
return mSelectedAccountView;
}
/**
* The |Continue As| button on the bottom sheet.
*/
ButtonCompat getContinueAsButton() {
return mContinueAsButton;
}
/**
* Expands the account list.
*/
void expandAccountList() {
mSelectedAccountView.setVisibility(View.GONE);
mContinueAsButton.setVisibility(View.GONE);
mAccountListView.setVisibility(View.VISIBLE);
}
/**
* Collapses the account list.
* If there is a non null selected account, the account list will collapse to that account,
* otherwise, the account list will just collapse the remaining.
*
* @param isSelectedAccountNonNull Flag indicates if the selected profile data exists
* in model.
*/
void collapseAccountList(boolean isSelectedAccountNonNull) {
mAccountListView.setVisibility(View.GONE);
mSelectedAccountView.setVisibility(isSelectedAccountNonNull ? View.VISIBLE : View.GONE);
mContinueAsButton.setVisibility(View.VISIBLE);
}
/**
* Updates the view of the collapsed account list.
*/
void updateCollapsedAccountList(DisplayableProfileData accountProfileData) {
if (accountProfileData == null) {
mContinueAsButton.setText(R.string.signin_add_account);
} else {
ExistingAccountRowViewBinder.bindAccountView(accountProfileData, mSelectedAccountView);
ImageView rowEndImage = mSelectedAccountView.findViewById(R.id.account_selection_mark);
rowEndImage.setImageResource(R.drawable.ic_expand_more_black_24dp);
rowEndImage.setColorFilter(R.color.default_icon_color);
String continueAsButtonText = mContext.getString(R.string.signin_promo_continue_as,
accountProfileData.getGivenNameOrFullNameOrEmail());
mContinueAsButton.setText(continueAsButtonText);
}
}
@Override
......@@ -49,7 +119,7 @@ class AccountPickerBottomSheetView implements BottomSheetContent {
@Override
public int getVerticalScrollOffset() {
return mAccountPickerItemView.computeVerticalScrollOffset();
return mAccountListView.computeVerticalScrollOffset();
}
@Override
......
......@@ -4,15 +4,38 @@
package org.chromium.chrome.browser.signin.account_picker;
import org.chromium.chrome.browser.signin.DisplayableProfileData;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel;
/**
* Stateless AccountPickerBottomSheet view binder.
*
* TODO(http://crbug.com/1081253): Implement the bottomsheet view binder.
*/
class AccountPickerBottomSheetViewBinder {
static void bind(
PropertyModel model, AccountPickerBottomSheetView view, PropertyKey propertyKey) {}
PropertyModel model, AccountPickerBottomSheetView view, PropertyKey propertyKey) {
if (propertyKey == AccountPickerBottomSheetProperties.ON_SELECTED_ACCOUNT_CLICKED) {
view.getSelectedAccountView().setOnClickListener(v -> {
model.get(AccountPickerBottomSheetProperties.ON_SELECTED_ACCOUNT_CLICKED).run();
});
} else if (propertyKey == AccountPickerBottomSheetProperties.IS_ACCOUNT_LIST_EXPANDED) {
if (model.get(AccountPickerBottomSheetProperties.IS_ACCOUNT_LIST_EXPANDED)) {
view.expandAccountList();
} else {
boolean isSelectedAccountNonNull =
model.get(AccountPickerBottomSheetProperties.SELECTED_ACCOUNT_DATA) != null;
view.collapseAccountList(isSelectedAccountNonNull);
}
} else if (propertyKey == AccountPickerBottomSheetProperties.SELECTED_ACCOUNT_DATA) {
DisplayableProfileData profileData =
model.get(AccountPickerBottomSheetProperties.SELECTED_ACCOUNT_DATA);
view.updateCollapsedAccountList(profileData);
} else if (propertyKey == AccountPickerBottomSheetProperties.ON_CONTINUE_AS_CLICKED) {
view.getContinueAsButton().setOnClickListener(v -> {
model.get(AccountPickerBottomSheetProperties.ON_CONTINUE_AS_CLICKED).run();
});
}
}
private AccountPickerBottomSheetViewBinder() {}
}
......@@ -27,7 +27,7 @@ import org.chromium.content_public.browser.UiThreadTaskTraits;
* It is responsible for the sign-in and adding account functions needed for the
* web sign-in flow.
*/
public class AccountPickerDelegate implements AccountPickerCoordinator.Listener {
public class AccountPickerDelegate {
private final ChromeActivity mChromeActivity;
private final Tab mTab;
private final String mContinueUrl;
......@@ -39,13 +39,9 @@ public class AccountPickerDelegate implements AccountPickerCoordinator.Listener
mContinueUrl = continueUrl;
}
/**
* Notifies that the user has selected an account.
*
* @param accountName The email of the selected account.
* @param isDefaultAccount Whether the selected account is the first in the account list.
* Signs the user into the account of the given accountName.
*/
@Override
public void onAccountSelected(String accountName, boolean isDefaultAccount) {
public void signIn(String accountName) {
Account account = AccountUtils.findAccountByName(
AccountManagerFacadeProvider.getInstance().tryGetGoogleAccounts(), accountName);
IdentityServicesProvider.get().getSigninManager().signIn(
......@@ -70,7 +66,6 @@ public class AccountPickerDelegate implements AccountPickerCoordinator.Listener
/**
* Notifies when the user clicked the "add account" button.
*/
@Override
public void addAccount() {
// TODO(https//crbug.com/1097031): We should select the added account
// and collapse the account chooser after the account is actually added.
......
......@@ -42,6 +42,25 @@ class ExistingAccountRowViewBinder {
-> model.get(ExistingAccountRowProperties.ON_CLICK_LISTENER)
.onResult(profileData));
} else if (propertyKey == ExistingAccountRowProperties.PROFILE_DATA) {
bindAccountView(profileData, view);
} else if (propertyKey == ExistingAccountRowProperties.IS_SELECTED_ACCOUNT) {
ImageView selectionMark = view.findViewById(R.id.account_selection_mark);
selectionMark.setVisibility(model.get(ExistingAccountRowProperties.IS_SELECTED_ACCOUNT)
? View.VISIBLE
: View.GONE);
} else {
throw new IllegalArgumentException(
"Cannot update the view for propertyKey: " + propertyKey);
}
}
/**
* Binds the view with the given profile data.
*
* @param profileData profile data needs to bind.
* @param view A view object inflated from @layout/account_picker_row.
*/
static void bindAccountView(DisplayableProfileData profileData, View view) {
ImageView accountImage = view.findViewById(R.id.account_image);
accountImage.setImageDrawable(profileData.getImage());
......@@ -58,14 +77,5 @@ class ExistingAccountRowViewBinder {
accountTextPrimary.setText(profileData.getAccountName());
accountTextSecondary.setVisibility(View.GONE);
}
} else if (propertyKey == ExistingAccountRowProperties.IS_SELECTED_ACCOUNT) {
ImageView selectionMark = view.findViewById(R.id.account_selection_mark);
selectionMark.setVisibility(model.get(ExistingAccountRowProperties.IS_SELECTED_ACCOUNT)
? View.VISIBLE
: View.GONE);
} else {
throw new IllegalArgumentException(
"Cannot update the view for propertyKey: " + propertyKey);
}
}
}
......@@ -19,7 +19,7 @@ import java.util.Map;
* returned by {@link #getProfileDataForAccount}.
*/
public class FakeProfileDataSource implements ProfileDataSource {
private final ObserverList<Observer> mObservers = new ObserverList<>();
protected final ObserverList<Observer> mObservers = new ObserverList<>();
private final Map<String, ProfileData> mProfileDataMap = new HashMap<>();
public FakeProfileDataSource() {}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment