Commit 52d613e7 authored by Boris Sazonov's avatar Boris Sazonov Committed by Commit Bot

[Unity][Android] Implement AccountPickerDialogFragment

This CL adds AccountPickerDialogFragment that is shown from
SigninFragmentBase and asks the user to select an account from the list.

Bug: 814728
Change-Id: Id82189694c5bf2b9c88ae3434e7a706539a493bc
Reviewed-on: https://chromium-review.googlesource.com/986262
Commit-Queue: Boris Sazonov <bsazonov@chromium.org>
Reviewed-by: default avatarTed Choc <tedchoc@chromium.org>
Cr-Commit-Position: refs/heads/master@{#548153}
parent 021f6279
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2018 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. -->
<android.support.v7.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp"/>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2018 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. -->
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableStart="@drawable/add_circle_blue"
android:drawablePadding="16dp"
android:gravity="center_vertical"
android:padding="8dp"
android:text="@string/signin_add_account"
android:textAppearance="@style/BlackBodyDefault"/>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2018 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. -->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp">
<ImageView
android:id="@+id/account_image"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginEnd="16dp"
android:contentDescription="@null"/>
<TextView
android:id="@+id/account_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toStartOf="@+id/account_selection_mark"
android:layout_toEndOf="@id/account_image"
android:gravity="center_vertical"
android:minHeight="20dp"
android:textAppearance="@style/BlackBodyDefault"
tools:text="John Doe"/>
<TextView
android:id="@+id/account_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/account_name"
android:layout_toStartOf="@+id/account_selection_mark"
android:layout_toEndOf="@id/account_image"
android:gravity="center_vertical"
android:minHeight="20dp"
android:textAppearance="@style/BlackCaption"
tools:text="john.doe@example.com"/>
<!-- TODO(https://crbug.com/828841): Use animated drawable for checkmark. -->
<ImageView
android:id="@+id/account_selection_mark"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginStart="16dp"
android:contentDescription="@null"
android:src="@drawable/ic_check_googblue_24dp"/>
</RelativeLayout>
// Copyright 2018 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;
import android.app.Dialog;
import android.os.Bundle;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.support.v4.util.ObjectsCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.chromium.base.Log;
import org.chromium.chrome.R;
import org.chromium.components.signin.AccountManagerDelegateException;
import org.chromium.components.signin.AccountManagerFacade;
import org.chromium.components.signin.AccountsChangeObserver;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
/**
* This class implements dialog-based account picker that is used by SigninFragmentBase. This
* fragment uses {@link Fragment#getParentFragment()} to report selection results, so parent
* fragment should:
* 1. Use {@link Fragment#getChildFragmentManager()} to add this fragment.
* 2. Implement {@link Callback} interface to get selection result.
*/
public class AccountPickerDialogFragment extends DialogFragment {
public interface Callback {
/**
* Notifies that 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.
*/
void onAccountSelected(String accountName, boolean isDefaultAccount);
}
@Retention(RetentionPolicy.SOURCE)
@IntDef({ViewType.EXISTING_ACCOUNT, ViewType.NEW_ACCOUNT})
private @interface ViewType {
int EXISTING_ACCOUNT = 0;
int NEW_ACCOUNT = 1;
}
private class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> {
class ViewHolder extends RecyclerView.ViewHolder {
private final @Nullable ImageView mAccountImage;
private final @Nullable TextView mAccountName;
private final @Nullable TextView mAccountEmail;
private final @Nullable ImageView mSelectionMark;
/** Used for displaying profile data for existing account. */
ViewHolder(View view, @Nullable ImageView accountImage, @Nullable TextView accountName,
@Nullable TextView accountEmail, @Nullable ImageView selectionMark) {
super(view);
mAccountImage = accountImage;
mAccountName = accountName;
mAccountEmail = accountEmail;
mSelectionMark = selectionMark;
}
/** Used for "Add account" row. */
ViewHolder(View view) {
this(view, null, null, null, null);
}
void onBind(DisplayableProfileData profileData, boolean isSelected) {
mAccountImage.setImageDrawable(profileData.getImage());
mAccountName.setText(profileData.getFullNameOrEmail());
mAccountEmail.setText(profileData.getAccountName());
mSelectionMark.setVisibility(isSelected ? View.VISIBLE : View.GONE);
}
}
private String mSelectedAccountName;
private List<DisplayableProfileData> mProfileDataList;
Adapter(String selectedAccountName, List<DisplayableProfileData> profileDataList) {
mSelectedAccountName = selectedAccountName;
mProfileDataList = profileDataList;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, @ViewType int viewType) {
LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
if (viewType == ViewType.NEW_ACCOUNT) {
View view =
inflater.inflate(R.layout.account_picker_new_account_row, viewGroup, false);
return new ViewHolder(view);
}
View view = inflater.inflate(R.layout.account_picker_row, viewGroup, false);
ImageView accountImage = (ImageView) view.findViewById(R.id.account_image);
TextView accountName = (TextView) view.findViewById(R.id.account_name);
TextView accountEmail = (TextView) view.findViewById(R.id.account_email);
ImageView selectionMark = (ImageView) view.findViewById(R.id.account_selection_mark);
return new ViewHolder(view, accountImage, accountName, accountEmail, selectionMark);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
switch (holder.getItemViewType()) {
case ViewType.EXISTING_ACCOUNT:
DisplayableProfileData profileData = mProfileDataList.get(position);
boolean isSelected = ObjectsCompat.equals(
profileData.getAccountName(), mSelectedAccountName);
holder.onBind(profileData, isSelected);
final String accountName = profileData.getAccountName();
final boolean isDefault = position == 0;
holder.itemView.setOnClickListener(
view -> onAccountSelected(accountName, isDefault));
return;
case ViewType.NEW_ACCOUNT:
// "Add account" row is immutable.
// TODO(https://crbug.com/814728): Add click listener.
return;
default:
assert false : "Unexpected view type!";
}
}
@Override
public int getItemCount() {
// The last row is "Add account" and doesn't have a corresponding account.
return mProfileDataList.size() + 1;
}
@Override
public int getItemViewType(int position) {
int result = position == mProfileDataList.size() ? ViewType.NEW_ACCOUNT
: ViewType.EXISTING_ACCOUNT;
return result;
}
void setSelectedAccountName(String selectedAccountName) {
mSelectedAccountName = selectedAccountName;
notifyDataSetChanged();
}
void setProfileDataList(List<DisplayableProfileData> profileDataList) {
mProfileDataList = profileDataList;
notifyDataSetChanged();
}
}
private static final String TAG = "AccountPickerDialog";
private static final String ARGUMENT_SELECTED_ACCOUNT_NAME =
"AccountPickerDialogFragment.SelectedAccountName";
private final AccountsChangeObserver mAccountsChangeObserver = this::updateAccounts;
private final ProfileDataCache.Observer mProfileDataObserver = accountId -> updateProfileData();
private ProfileDataCache mProfileDataCache;
private List<String> mAccounts;
private Adapter mAdapter;
/**
* Creates an instance and sets its arguments.
* @param selectedAccountName The name of the account that should be marked as selected.
*/
public static AccountPickerDialogFragment create(@Nullable String selectedAccountName) {
AccountPickerDialogFragment result = new AccountPickerDialogFragment();
Bundle args = new Bundle();
args.putString(ARGUMENT_SELECTED_ACCOUNT_NAME, selectedAccountName);
result.setArguments(args);
return result;
}
public AccountPickerDialogFragment() {
// Fragment must have a publicly accessible default constructor
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
assert getCallback() != null : "No callback for AccountPickerDialogFragment";
mProfileDataCache = new ProfileDataCache(
getActivity(), getResources().getDimensionPixelSize(R.dimen.user_picture_size));
String selectedAccountName = getArguments().getString(ARGUMENT_SELECTED_ACCOUNT_NAME);
// Account list will be updated in onStart()
mAdapter = new Adapter(selectedAccountName, new ArrayList<>());
}
@Override
public void onStart() {
super.onStart();
AccountManagerFacade.get().addObserver(mAccountsChangeObserver);
mProfileDataCache.addObserver(mProfileDataObserver);
updateAccounts();
}
@Override
public void onStop() {
super.onStop();
mProfileDataCache.removeObserver(mProfileDataObserver);
AccountManagerFacade.get().removeObserver(mAccountsChangeObserver);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder =
new AlertDialog.Builder(getActivity(), R.style.AlertDialogTheme);
LayoutInflater inflater = LayoutInflater.from(builder.getContext());
RecyclerView recyclerView =
(RecyclerView) inflater.inflate(R.layout.account_picker_dialog_body, null);
recyclerView.setAdapter(mAdapter);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
return builder.setTitle(R.string.signin_account_picker_dialog_title)
.setView(recyclerView)
.create();
}
/**
* Updates the selected account.
* @param selectedAccountName The name of the account that should be marked as selected.
*/
public void updateSelectedAccount(String selectedAccountName) {
mAdapter.setSelectedAccountName(selectedAccountName);
}
private void onAccountSelected(String accountName, boolean isDefaultAccount) {
getCallback().onAccountSelected(accountName, isDefaultAccount);
dismiss();
}
private void updateAccounts() {
try {
mAccounts = AccountManagerFacade.get().getGoogleAccountNames();
} catch (AccountManagerDelegateException ex) {
Log.e(TAG, "Can't get account list", ex);
dismiss();
return;
}
mProfileDataCache.update(mAccounts);
updateProfileData();
}
private void updateProfileData() {
List<DisplayableProfileData> profileDataList = new ArrayList<>();
for (String accountName : mAccounts) {
profileDataList.add(mProfileDataCache.getProfileDataOrDefault(accountName));
}
mAdapter.setProfileDataList(profileDataList);
}
private Callback getCallback() {
return (Callback) getParentFragment();
}
}
...@@ -9,6 +9,7 @@ import android.os.SystemClock; ...@@ -9,6 +9,7 @@ import android.os.SystemClock;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater; import android.view.LayoutInflater;
...@@ -40,13 +41,16 @@ import java.util.concurrent.TimeUnit; ...@@ -40,13 +41,16 @@ import java.util.concurrent.TimeUnit;
* features. Configuration for this fragment is provided by overriding {@link #getSigninArguments} * features. Configuration for this fragment is provided by overriding {@link #getSigninArguments}
* derived classes. * derived classes.
*/ */
public abstract class SigninFragmentBase extends Fragment { public abstract class SigninFragmentBase
extends Fragment implements AccountPickerDialogFragment.Callback {
private static final String TAG = "SigninFragmentBase"; private static final String TAG = "SigninFragmentBase";
private static final String SETTINGS_LINK_OPEN = "<LINK1>"; private static final String SETTINGS_LINK_OPEN = "<LINK1>";
private static final String SETTINGS_LINK_CLOSE = "</LINK1>"; private static final String SETTINGS_LINK_CLOSE = "</LINK1>";
private static final String ARGUMENT_ACCESS_POINT = "SigninFragmentBase.AccessPoint"; private static final String ARGUMENT_ACCESS_POINT = "SigninFragmentBase.AccessPoint";
public static final String ACCOUNT_PICKER_DIALOG_TAG =
"SigninFragmentBase.AccountPickerDialogFragment";
private @SigninAccessPoint int mSigninAccessPoint; private @SigninAccessPoint int mSigninAccessPoint;
// TODO(https://crbug.com/814728): Pass this as Fragment argument. // TODO(https://crbug.com/814728): Pass this as Fragment argument.
...@@ -154,6 +158,8 @@ public abstract class SigninFragmentBase extends Fragment { ...@@ -154,6 +158,8 @@ public abstract class SigninFragmentBase extends Fragment {
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mView = (SigninView) inflater.inflate(R.layout.signin_view, container, false); mView = (SigninView) inflater.inflate(R.layout.signin_view, container, false);
mView.getAccountPickerView().setOnClickListener(view -> onAccountPickerClicked());
mView.getAcceptButton().setVisibility(View.GONE); mView.getAcceptButton().setVisibility(View.GONE);
mView.getMoreButton().setVisibility(View.VISIBLE); mView.getMoreButton().setVisibility(View.VISIBLE);
mView.getMoreButton().setOnClickListener(view -> { mView.getMoreButton().setOnClickListener(view -> {
...@@ -226,6 +232,11 @@ public abstract class SigninFragmentBase extends Fragment { ...@@ -226,6 +232,11 @@ public abstract class SigninFragmentBase extends Fragment {
mView.getRefuseButton().setEnabled(enabled); mView.getRefuseButton().setEnabled(enabled);
} }
private void onAccountPickerClicked() {
// TODO(https://crbug.com/814728): Ignore clicks if GMS reported an error.
showAccountPicker();
}
/** /**
* Records the Sync consent. * Records the Sync consent.
* @param confirmationView The view that the user clicked when consenting. * @param confirmationView The view that the user clicked when consenting.
...@@ -235,6 +246,22 @@ public abstract class SigninFragmentBase extends Fragment { ...@@ -235,6 +246,22 @@ public abstract class SigninFragmentBase extends Fragment {
ConsentAuditorFeature.CHROME_SYNC, confirmationView, mView); ConsentAuditorFeature.CHROME_SYNC, confirmationView, mView);
} }
private void showAccountPicker() {
// Account picker is already shown
if (getChildFragmentManager().findFragmentByTag(ACCOUNT_PICKER_DIALOG_TAG) != null) return;
AccountPickerDialogFragment dialog =
AccountPickerDialogFragment.create(mSelectedAccountName);
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
transaction.add(dialog, ACCOUNT_PICKER_DIALOG_TAG);
transaction.commit();
}
@Override
public void onAccountSelected(String accountName, boolean isDefaultAccount) {
selectAccount(accountName, isDefaultAccount);
}
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
...@@ -258,7 +285,12 @@ public abstract class SigninFragmentBase extends Fragment { ...@@ -258,7 +285,12 @@ public abstract class SigninFragmentBase extends Fragment {
mProfileDataCache.update(Collections.singletonList(mSelectedAccountName)); mProfileDataCache.update(Collections.singletonList(mSelectedAccountName));
updateProfileData(); updateProfileData();
// TODO(https://crbug.com/814728): Update selected account in account picker. AccountPickerDialogFragment accountPickerFragment =
(AccountPickerDialogFragment) getChildFragmentManager().findFragmentByTag(
ACCOUNT_PICKER_DIALOG_TAG);
if (accountPickerFragment != null) {
accountPickerFragment.updateSelectedAccount(accountName);
}
} }
private void triggerUpdateAccounts() { private void triggerUpdateAccounts() {
...@@ -293,7 +325,7 @@ public abstract class SigninFragmentBase extends Fragment { ...@@ -293,7 +325,7 @@ public abstract class SigninFragmentBase extends Fragment {
// TODO(https://crbug.com/814728): Cancel ConfirmSyncDataStateMachine. // TODO(https://crbug.com/814728): Cancel ConfirmSyncDataStateMachine.
selectAccount(mAccountNames.get(0), true); selectAccount(mAccountNames.get(0), true);
// TODO(https://crbug.com/814728): Show account picker. showAccountPicker(); // Show account picker so user can confirm the account selection.
} }
@Nullable @Nullable
......
...@@ -20,6 +20,7 @@ import org.chromium.ui.widget.ButtonCompat; ...@@ -20,6 +20,7 @@ import org.chromium.ui.widget.ButtonCompat;
public class SigninView extends LinearLayout { public class SigninView extends LinearLayout {
private SigninScrollView mScrollView; private SigninScrollView mScrollView;
private TextView mTitle; private TextView mTitle;
private View mAccountPicker;
private ImageView mAccountImage; private ImageView mAccountImage;
private TextView mAccountName; private TextView mAccountName;
private TextView mAccountEmail; private TextView mAccountEmail;
...@@ -42,6 +43,7 @@ public class SigninView extends LinearLayout { ...@@ -42,6 +43,7 @@ public class SigninView extends LinearLayout {
mScrollView = (SigninScrollView) findViewById(R.id.signin_scroll_view); mScrollView = (SigninScrollView) findViewById(R.id.signin_scroll_view);
mTitle = (TextView) findViewById(R.id.signin_title); mTitle = (TextView) findViewById(R.id.signin_title);
mAccountPicker = findViewById(R.id.signin_account_picker);
mAccountImage = (ImageView) findViewById(R.id.account_image); mAccountImage = (ImageView) findViewById(R.id.account_image);
mAccountName = (TextView) findViewById(R.id.account_name); mAccountName = (TextView) findViewById(R.id.account_name);
mAccountEmail = (TextView) findViewById(R.id.account_email); mAccountEmail = (TextView) findViewById(R.id.account_email);
...@@ -65,6 +67,10 @@ public class SigninView extends LinearLayout { ...@@ -65,6 +67,10 @@ public class SigninView extends LinearLayout {
return mTitle; return mTitle;
} }
public View getAccountPickerView() {
return mAccountPicker;
}
public ImageView getAccountImageView() { public ImageView getAccountImageView() {
return mAccountImage; return mAccountImage;
} }
......
...@@ -2315,6 +2315,9 @@ Google may use your browsing activity, content on some sites you visit, and othe ...@@ -2315,6 +2315,9 @@ Google may use your browsing activity, content on some sites you visit, and othe
<message name="IDS_SIGNIN_ACCEPT_BUTTON" desc="Text for the confirmation button in the sign-in screen. By clicking this button users signs in and turns on Sync and personalization. [CHAR-LIMIT=20]" translateable="false"> <message name="IDS_SIGNIN_ACCEPT_BUTTON" desc="Text for the confirmation button in the sign-in screen. By clicking this button users signs in and turns on Sync and personalization. [CHAR-LIMIT=20]" translateable="false">
Yes, I'm in Yes, I'm in
</message> </message>
<message name="IDS_SIGNIN_ACCOUNT_PICKER_DIALOG_TITLE" desc="The title for the dialog that shows the list of accounts on the device and asks user to select one of these accounts. [CHAR-LIMIT=27]" translateable="false">
Choose an account
</message>
<!-- Personalized Signin Promos Strings --> <!-- Personalized Signin Promos Strings -->
<message name="IDS_SIGNIN_PROMO_DESCRIPTION_BOOKMARKS" desc="Description string for 'Continue as' signin promo shown in Bookmarks screen."> <message name="IDS_SIGNIN_PROMO_DESCRIPTION_BOOKMARKS" desc="Description string for 'Continue as' signin promo shown in Bookmarks screen.">
......
...@@ -1120,6 +1120,7 @@ chrome_java_sources = [ ...@@ -1120,6 +1120,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/signin/AccountAdder.java", "java/src/org/chromium/chrome/browser/signin/AccountAdder.java",
"java/src/org/chromium/chrome/browser/signin/AccountManagementFragment.java", "java/src/org/chromium/chrome/browser/signin/AccountManagementFragment.java",
"java/src/org/chromium/chrome/browser/signin/AccountManagementScreenHelper.java", "java/src/org/chromium/chrome/browser/signin/AccountManagementScreenHelper.java",
"java/src/org/chromium/chrome/browser/signin/AccountPickerDialogFragment.java",
"java/src/org/chromium/chrome/browser/signin/AccountSigninActivity.java", "java/src/org/chromium/chrome/browser/signin/AccountSigninActivity.java",
"java/src/org/chromium/chrome/browser/signin/AccountSigninChooseView.java", "java/src/org/chromium/chrome/browser/signin/AccountSigninChooseView.java",
"java/src/org/chromium/chrome/browser/signin/AccountSigninConfirmationView.java", "java/src/org/chromium/chrome/browser/signin/AccountSigninConfirmationView.java",
......
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