| // Copyright 2019 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.autofill_assistant.payment; |
| |
| import android.content.Context; |
| import android.support.annotation.Nullable; |
| import android.text.TextUtils; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.LinearLayout; |
| import android.widget.TextView; |
| |
| import org.chromium.base.Callback; |
| import org.chromium.chrome.autofill_assistant.R; |
| import org.chromium.chrome.browser.payments.ui.SectionInformation; |
| import org.chromium.chrome.browser.widget.prefeditor.EditableOption; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * This is the generic superclass for all autofill-assistant payment request sections. |
| * |
| * @param <T> The type of |EditableOption| that a concrete instance of this class is created for, |
| * such as |AutofillContact|, |AutofillPaymentMethod|, etc. |
| */ |
| public abstract class AssistantPaymentRequestSection<T extends EditableOption> { |
| private final View mTitleAddButton; |
| private final AssistantVerticalExpander mSectionExpander; |
| private final AssistantChoiceList mItemsView; |
| private final View mSummaryView; |
| private final int mFullViewResId; |
| private final List<Item> mItems; |
| private final int mTitleToContentPadding; |
| |
| protected final Context mContext; |
| protected T mSelectedOption; |
| |
| private boolean mIgnoreItemSelectedNotifications; |
| private Callback<T> mListener; |
| private int mTopPadding; |
| private int mBottomPadding; |
| |
| private class Item { |
| Item(View fullView, T option) { |
| this.mFullView = fullView; |
| this.mOption = option; |
| } |
| View mFullView; |
| T mOption; |
| } |
| |
| /** |
| * |
| * @param context The context to use. |
| * @param parent The parent view to add this payment request section to. |
| * @param summaryViewResId The resource ID of the summary view to inflate. |
| * @param fullViewResId The resource ID of the full view to inflate. |
| * @param titleToContentPadding The amount of padding between title and content views. |
| * @param title The title string to display. |
| * @param titleAddButton The string to display in the title add button. |
| * @param listAddButton The string to display in the add button at the bottom of the list. |
| */ |
| public AssistantPaymentRequestSection(Context context, ViewGroup parent, int summaryViewResId, |
| int fullViewResId, int titleToContentPadding, String title, String titleAddButton, |
| String listAddButton) { |
| mContext = context; |
| mFullViewResId = fullViewResId; |
| mItems = new ArrayList<>(); |
| mTitleToContentPadding = titleToContentPadding; |
| |
| LayoutInflater inflater = LayoutInflater.from(context); |
| mSectionExpander = new AssistantVerticalExpander(context, null); |
| View sectionTitle = |
| inflater.inflate(R.layout.autofill_assistant_payment_request_section_title, null); |
| mSummaryView = inflater.inflate(summaryViewResId, null); |
| mItemsView = (AssistantChoiceList) inflater.inflate( |
| R.layout.autofill_assistant_payment_request_choice_list, null); |
| mItemsView.setAddButtonLabel(listAddButton); |
| |
| mSectionExpander.setTitleView(sectionTitle, |
| new LinearLayout.LayoutParams( |
| ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); |
| mSectionExpander.setCollapsedView(mSummaryView, |
| new LinearLayout.LayoutParams( |
| ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); |
| mSectionExpander.setExpandedView(mItemsView, |
| new LinearLayout.LayoutParams( |
| ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); |
| |
| // Adjust margins such that title and collapsed views are indented, but expanded view is |
| // full-width. |
| int horizontalMargin = context.getResources().getDimensionPixelSize( |
| R.dimen.autofill_assistant_bottombar_horizontal_spacing); |
| setHorizontalMargins(sectionTitle, horizontalMargin, horizontalMargin); |
| setHorizontalMargins(mSectionExpander.getChevronButton(), 0, horizontalMargin); |
| setHorizontalMargins(mSummaryView, horizontalMargin, 0); |
| setHorizontalMargins(mItemsView, 0, 0); |
| |
| TextView titleView = mSectionExpander.findViewById(R.id.section_title); |
| titleView.setText(title); |
| |
| TextView titleAddButtonLabelView = |
| mSectionExpander.findViewById(R.id.section_title_add_button_label); |
| titleAddButtonLabelView.setText(titleAddButton); |
| |
| mTitleAddButton = mSectionExpander.findViewById(R.id.section_title_add_button); |
| mTitleAddButton.setOnClickListener(unusedView -> createOrEditItem(null, null)); |
| |
| mItemsView.setOnAddButtonClickedListener(() -> createOrEditItem(null, null)); |
| parent.addView(mSectionExpander, |
| new ViewGroup.LayoutParams( |
| ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); |
| updateVisibility(); |
| } |
| |
| View getView() { |
| return mSectionExpander; |
| } |
| |
| void setVisible(boolean visible) { |
| mSectionExpander.setVisibility(visible ? View.VISIBLE : View.GONE); |
| } |
| |
| void setListener(Callback<T> listener) { |
| mListener = listener; |
| } |
| |
| /** |
| * Replaces the set of displayed items. |
| * |
| * @param options The new items. |
| * @param selectedItemIndex The index of the item in |items| to select. If < 0, the first |
| * complete item will automatically be selected. |
| */ |
| void setItems(List<T> options, int selectedItemIndex) { |
| // Automatically pre-select unless already specified outside. |
| if (selectedItemIndex < 0 && !options.isEmpty()) { |
| for (int i = 0; i < options.size(); i++) { |
| if (options.get(i).isComplete()) { |
| selectedItemIndex = i; |
| } |
| } |
| // Fallback: if there are no complete items, select the first (incomplete) one. |
| if (selectedItemIndex == SectionInformation.NO_SELECTION) { |
| selectedItemIndex = 0; |
| } |
| } |
| |
| Item initiallySelectedItem = null; |
| mItems.clear(); |
| mItemsView.clearItems(); |
| mSelectedOption = null; |
| for (int i = 0; i < options.size(); i++) { |
| Item item = createItem(options.get(i)); |
| addItem(item); |
| |
| if (i == selectedItemIndex) { |
| initiallySelectedItem = item; |
| } |
| } |
| updateVisibility(); |
| |
| if (initiallySelectedItem != null) { |
| mIgnoreItemSelectedNotifications = true; |
| selectItem(initiallySelectedItem); |
| mIgnoreItemSelectedNotifications = false; |
| } |
| } |
| |
| void addOrUpdateItem(@Nullable T option, boolean select) { |
| if (option == null) { |
| return; |
| } |
| |
| // Update existing item if possible. |
| Item item = null; |
| for (int i = 0; i < mItems.size(); i++) { |
| if (TextUtils.equals(mItems.get(i).mOption.getIdentifier(), option.getIdentifier())) { |
| item = mItems.get(i); |
| item.mOption = option; |
| updateFullView(item.mFullView, item.mOption); |
| break; |
| } |
| } |
| |
| if (item == null) { |
| item = createItem(option); |
| addItem(item); |
| } else { |
| updateSummaryView(mSummaryView, item.mOption); |
| } |
| onItemAddedOrUpdated(option); |
| |
| if (select) { |
| mIgnoreItemSelectedNotifications = true; |
| selectItem(item); |
| mIgnoreItemSelectedNotifications = false; |
| } |
| } |
| |
| void setPaddings(int topPadding, int bottomPadding) { |
| mTopPadding = topPadding; |
| mBottomPadding = bottomPadding; |
| updatePaddings(); |
| } |
| |
| void updatePaddings() { |
| if (isEmpty()) { |
| // Section is empty, i.e., the title is the bottom-most widget. |
| mSectionExpander.setTitlePadding(mTopPadding, mBottomPadding); |
| } else if (mSectionExpander.isExpanded()) { |
| // Section is expanded, i.e., the expanded widget is the bottom-most widget. |
| mSectionExpander.setTitlePadding(mTopPadding, mTitleToContentPadding); |
| setBottomPadding(mSectionExpander.getExpandedView(), mBottomPadding); |
| } else { |
| // Section is non-empty and collapsed -> collapsed widget is the bottom-most widget. |
| mSectionExpander.setTitlePadding(mTopPadding, mTitleToContentPadding); |
| setBottomPadding(mSectionExpander.getCollapsedView(), mBottomPadding); |
| } |
| } |
| |
| private void selectItem(Item item) { |
| mSelectedOption = item.mOption; |
| mItemsView.setCheckedItem(item.mFullView); |
| updateSummaryView(mSummaryView, item.mOption); |
| updateVisibility(); |
| |
| if (mListener != null) { |
| mListener.onResult( |
| item.mOption != null && item.mOption.isComplete() ? item.mOption : null); |
| } |
| } |
| |
| /** |
| * Creates a new |Item| from |option|. |
| */ |
| private Item createItem(T option) { |
| View fullView = LayoutInflater.from(mContext).inflate(mFullViewResId, null); |
| updateFullView(fullView, option); |
| Item item = new Item(fullView, option); |
| return item; |
| } |
| |
| /** |
| * Adds |item| to the UI. |
| */ |
| private void addItem(Item item) { |
| mItems.add(item); |
| mItemsView.addItem(item.mFullView, /*hasEditButton=*/true, selected -> { |
| if (mIgnoreItemSelectedNotifications || !selected) { |
| return; |
| } |
| mIgnoreItemSelectedNotifications = true; |
| selectItem(item.mFullView, item.mOption); |
| mIgnoreItemSelectedNotifications = false; |
| if (item.mOption.isComplete()) { |
| // Workaround for Android bug: a layout transition may cause the newly checked |
| // radiobutton to not render properly. |
| mSectionExpander.post(() -> mSectionExpander.setExpanded(false)); |
| } else { |
| createOrEditItem(item.mFullView, item.mOption); |
| } |
| }, () -> createOrEditItem(item.mFullView, item.mOption)); |
| updateVisibility(); |
| } |
| |
| /** |
| * Asks the subclass to edit an item or create a new one (if |oldItem| is null). Subclasses |
| * should call |onItemCreatedOrEdited| when they are done. |
| * @param oldFullView The view associated with |oldItem|. |
| * @param oldItem The item to be edited (null if a new item should be created). |
| */ |
| protected abstract void createOrEditItem(@Nullable View oldFullView, @Nullable T oldItem); |
| |
| /** |
| * Asks the subclass to update the contents of |fullView|, which was previously created by |
| * |createFullView|. |
| */ |
| protected abstract void updateFullView(View fullView, T option); |
| |
| /** |
| * Asks the subclass to update the contents of the summary view. |
| */ |
| protected abstract void updateSummaryView(View summaryView, T option); |
| |
| /** |
| * An item was added to the list or updated in place. Subclasses may react to this event. A |
| * common use case is for subclasses to add the information of the new profile to the |
| * autocomplete fields of their editor. |
| */ |
| protected abstract void onItemAddedOrUpdated(T option); |
| |
| /** |
| * For convenience. Hides |view| if it is empty. |
| */ |
| void hideIfEmpty(TextView view) { |
| view.setVisibility(view.length() == 0 ? View.GONE : View.VISIBLE); |
| } |
| |
| /** |
| * An old item was edited or a new item was created. |
| * |
| * @param oldItem The item that was edited. null to indicate that a new item was created. |
| * @param oldFullView The view associated with |oldItem|. Null if |oldItem| is null. |
| * @param newItem The new or edited item. Cancelling an 'edit' flow will yield the old item. |
| */ |
| void onItemCreatedOrEdited( |
| @Nullable T oldItem, @Nullable View oldFullView, @Nullable T newItem) { |
| if (newItem == null) { |
| // User cancelled 'add' flow. |
| return; |
| } else if (!newItem.isComplete()) { |
| // User cancelled 'edit' flow. |
| return; |
| // TODO(crbug.com/806868) add special case for cancelling edit of incomplete item |
| } else { |
| addOrUpdateItem(newItem, /*select = */ true); |
| } |
| } |
| |
| private boolean isEmpty() { |
| return mItems.isEmpty(); |
| } |
| |
| private void setHorizontalMargins(View view, int marginStart, int marginEnd) { |
| ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); |
| lp.setMarginStart(marginStart); |
| lp.setMarginEnd(marginEnd); |
| view.setLayoutParams(lp); |
| } |
| |
| private void selectItem(View fullView, T option) { |
| mItemsView.setCheckedItem(fullView); |
| updateSummaryView(mSummaryView, option); |
| updateVisibility(); |
| |
| if (mListener != null) { |
| mListener.onResult(option != null && option.isComplete() ? option : null); |
| } |
| } |
| |
| private void updateVisibility() { |
| mTitleAddButton.setVisibility(isEmpty() ? View.VISIBLE : View.GONE); |
| mSectionExpander.setFixed(isEmpty()); |
| mSectionExpander.setCollapsedVisible(!isEmpty()); |
| mSectionExpander.setExpandedVisible(!isEmpty()); |
| if (isEmpty()) { |
| mSectionExpander.setExpanded(false); |
| } |
| updatePaddings(); |
| } |
| |
| private void setBottomPadding(View view, int padding) { |
| view.setPadding( |
| view.getPaddingLeft(), view.getPaddingTop(), view.getPaddingRight(), padding); |
| } |
| } |