diff options
Diffstat (limited to 'chromium/components/infobars')
32 files changed, 1863 insertions, 3 deletions
diff --git a/chromium/components/infobars/android/BUILD.gn b/chromium/components/infobars/android/BUILD.gn new file mode 100644 index 00000000000..70e8674eeba --- /dev/null +++ b/chromium/components/infobars/android/BUILD.gn @@ -0,0 +1,70 @@ +# 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. + +import("//build/config/android/rules.gni") + +android_resources("java_resources") { + sources = [ + "res/drawable-hdpi/infobar_shadow_left.9.png", + "res/drawable-hdpi/infobar_shadow_top.png", + "res/drawable-mdpi/infobar_shadow_left.9.png", + "res/drawable-mdpi/infobar_shadow_top.png", + "res/drawable-xhdpi/infobar_shadow_left.9.png", + "res/drawable-xhdpi/infobar_shadow_top.png", + "res/drawable-xxhdpi/infobar_shadow_left.9.png", + "res/drawable-xxhdpi/infobar_shadow_top.png", + "res/drawable-xxxhdpi/infobar_shadow_left.9.png", + "res/drawable-xxxhdpi/infobar_shadow_top.png", + "res/layout/infobar_control_icon_with_description.xml", + "res/layout/infobar_control_message.xml", + "res/layout/infobar_control_spinner.xml", + "res/layout/infobar_control_spinner_drop_down.xml", + "res/layout/infobar_control_spinner_view.xml", + "res/layout/infobar_control_toggle.xml", + "res/values/dimens.xml", + "res/values/ids.xml", + ] + custom_package = "org.chromium.components.infobars" + deps = [ + "//components/browser_ui/widget/android:java_resources", + "//components/strings", + "//ui/android:ui_java_resources", + ] +} + +android_library("java") { + sources = [ + "java/src/org/chromium/components/infobars/InfoBarControlLayout.java", + "java/src/org/chromium/components/infobars/InfoBarInteractionHandler.java", + "java/src/org/chromium/components/infobars/InfoBarLayout.java", + "java/src/org/chromium/components/infobars/InfoBarMessageView.java", + ] + deps = [ + ":java_resources", + "//base:base_java", + "//components/browser_ui/widget/android:java", + "//third_party/android_deps:androidx_appcompat_appcompat_java", + "//third_party/android_deps:androidx_appcompat_appcompat_resources_java", + "//ui/android:ui_java", + ] +} + +android_library("javatests") { + testonly = true + sources = [ + "java/src/org/chromium/components/infobars/InfoBarControlLayoutTest.java", + ] + deps = [ + ":java", + "//base:base_java", + "//base:base_java_test_support", + "//components/browser_ui/styles/android:java_resources", + "//components/browser_ui/widget/android:java", + "//third_party/android_deps:androidx_annotation_annotation_java", + "//third_party/android_deps:androidx_appcompat_appcompat_java", + "//third_party/android_support_test_runner:rules_java", + "//third_party/android_support_test_runner:runner_java", + "//third_party/junit:junit", + ] +} diff --git a/chromium/components/infobars/android/DEPS b/chromium/components/infobars/android/DEPS new file mode 100644 index 00000000000..c08ee06f6bd --- /dev/null +++ b/chromium/components/infobars/android/DEPS @@ -0,0 +1,3 @@ +include_rules = [ + "+components/browser_ui/widget/android", +] diff --git a/chromium/components/infobars/android/OWNERS b/chromium/components/infobars/android/OWNERS new file mode 100644 index 00000000000..0c68d145004 --- /dev/null +++ b/chromium/components/infobars/android/OWNERS @@ -0,0 +1,10 @@ +# Primary: +pavely@chromium.org + +# Secondary: +dtrainor@chromium.org +mdjones@chromium.org +twellington@chromium.org + +# COMPONENT: UI>Browser>Mobile>Messages +# OS: Android diff --git a/chromium/components/infobars/android/java/src/org/chromium/components/infobars/InfoBarControlLayout.java b/chromium/components/infobars/android/java/src/org/chromium/components/infobars/InfoBarControlLayout.java new file mode 100644 index 00000000000..7650b9af9c9 --- /dev/null +++ b/chromium/components/infobars/android/java/src/org/chromium/components/infobars/InfoBarControlLayout.java @@ -0,0 +1,507 @@ +// Copyright 2015 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.components.infobars; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Paint; +import android.text.method.LinkMovementMethod; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.widget.SwitchCompat; + +import org.chromium.base.ApiCompatibilityUtils; +import org.chromium.base.StrictModeContext; +import org.chromium.components.browser_ui.widget.DualControlLayout; +import org.chromium.components.browser_ui.widget.RadioButtonLayout; + +import java.util.List; + +/** + * Lays out a group of controls (e.g. switches, spinners, or additional text) for InfoBars that need + * more than the normal pair of buttons. + * + * This class works with the {@link InfoBarLayout} to define a standard set of controls with + * standardized spacings and text styling that gets laid out in grid form: https://crbug.com/543205 + * + * Manually specified margins on the children managed by this layout are EXPLICITLY ignored to + * enforce a uniform margin between controls across all InfoBar types. Do NOT circumvent this + * restriction with creative layout definitions. If the layout algorithm doesn't work for your new + * InfoBar, convince Chrome for Android's UX team to amend the master spec and then change the + * layout algorithm to match. + * + * TODO(dfalcantara): The line spacing multiplier is applied to all lines in JB & KK, even if the + * TextView has only one line. This throws off vertical alignment. Find a + * solution that hopefully doesn't involve subclassing the TextView. + */ +public final class InfoBarControlLayout extends ViewGroup { + /** + * ArrayAdapter that automatically determines what size make its Views to accommodate all of + * its potential values. + * @param <T> Type of object that the ArrayAdapter stores. + */ + public static final class InfoBarArrayAdapter<T> extends ArrayAdapter<T> { + private final String mLabel; + private int mMinWidthRequiredForValues; + + public InfoBarArrayAdapter(Context context, String label) { + super(context, R.layout.infobar_control_spinner_drop_down); + mLabel = label; + } + + public InfoBarArrayAdapter(Context context, T[] objects) { + super(context, R.layout.infobar_control_spinner_drop_down, objects); + mLabel = null; + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + TextView view; + if (convertView instanceof TextView) { + view = (TextView) convertView; + } else { + view = (TextView) inflateLayout( + getContext(), R.layout.infobar_control_spinner_drop_down, parent); + } + + view.setText(getItem(position).toString()); + return view; + } + + @Override + public DualControlLayout getView(int position, View convertView, ViewGroup parent) { + DualControlLayout view; + if (convertView instanceof DualControlLayout) { + view = (DualControlLayout) convertView; + } else { + view = (DualControlLayout) inflateLayout( + getContext(), R.layout.infobar_control_spinner_view, parent); + } + + // Set up the spinner label. The text it displays won't change. + TextView labelView = (TextView) view.getChildAt(0); + labelView.setText(mLabel); + + // Because the values can be of different widths, the TextView may expand or shrink. + // Enforcing a minimum width prevents the layout from doing so as the user swaps values, + // preventing unwanted layout passes. + TextView valueView = (TextView) view.getChildAt(1); + valueView.setText(getItem(position).toString()); + valueView.setMinimumWidth(mMinWidthRequiredForValues); + + return view; + } + + /** + * Computes and records the minimum width required to display any of the values without + * causing another layout pass when switching values. + */ + int computeMinWidthRequiredForValues() { + DualControlLayout layout = getView(0, null, null); + TextView container = (TextView) layout.getChildAt(1); + + Paint textPaint = container.getPaint(); + float longestLanguageWidth = 0; + for (int i = 0; i < getCount(); i++) { + float width = textPaint.measureText(getItem(i).toString()); + longestLanguageWidth = Math.max(longestLanguageWidth, width); + } + + mMinWidthRequiredForValues = (int) Math.ceil(longestLanguageWidth); + return mMinWidthRequiredForValues; + } + + /** + * Explicitly sets the minimum width required to display all of the values. + */ + void setMinWidthRequiredForValues(int requiredWidth) { + mMinWidthRequiredForValues = requiredWidth; + } + } + + /** + * Extends the regular LayoutParams by determining where a control should be located. + */ + @VisibleForTesting + static final class ControlLayoutParams extends LayoutParams { + public int start; + public int top; + public int columnsRequired; + private boolean mMustBeFullWidth; + + /** + * Stores values required for laying out this ViewGroup's children. + * + * This is set up as a private method to mitigate attempts at adding controls to the layout + * that aren't provided by the InfoBarControlLayout. + */ + private ControlLayoutParams() { + super(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } + } + + private final int mMarginBetweenRows; + private final int mMarginBetweenColumns; + + /** + * Do not call this method directly; use {@link InfoBarLayout#addControlLayout()}. + */ + public InfoBarControlLayout(Context context) { + this(context, null); + } + + public InfoBarControlLayout(Context context, AttributeSet attrs) { + super(context, attrs); + + Resources resources = context.getResources(); + mMarginBetweenRows = + resources.getDimensionPixelSize(R.dimen.infobar_control_margin_between_rows); + mMarginBetweenColumns = + resources.getDimensionPixelSize(R.dimen.infobar_control_margin_between_columns); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int fullWidth = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED + ? Integer.MAX_VALUE + : MeasureSpec.getSize(widthMeasureSpec); + int columnWidth = Math.max(0, (fullWidth - mMarginBetweenColumns) / 2); + + int atMostFullWidthSpec = MeasureSpec.makeMeasureSpec(fullWidth, MeasureSpec.AT_MOST); + int exactlyFullWidthSpec = MeasureSpec.makeMeasureSpec(fullWidth, MeasureSpec.EXACTLY); + int exactlyColumnWidthSpec = MeasureSpec.makeMeasureSpec(columnWidth, MeasureSpec.EXACTLY); + int unspecifiedSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + + // Figure out how many columns each child requires. + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + measureChild(child, atMostFullWidthSpec, unspecifiedSpec); + + if (child.getMeasuredWidth() <= columnWidth + && !getControlLayoutParams(child).mMustBeFullWidth) { + getControlLayoutParams(child).columnsRequired = 1; + } else { + getControlLayoutParams(child).columnsRequired = 2; + } + } + + // Pack all the children as tightly into rows as possible without changing their ordering. + // Stretch out column-width controls if either it is the last control or the next one is + // a full-width control. + for (int i = 0; i < getChildCount(); i++) { + ControlLayoutParams lp = getControlLayoutParams(getChildAt(i)); + + if (i == getChildCount() - 1) { + lp.columnsRequired = 2; + } else { + ControlLayoutParams nextLp = getControlLayoutParams(getChildAt(i + 1)); + if (lp.columnsRequired + nextLp.columnsRequired > 2) { + // This control is too big to place with the next child. + lp.columnsRequired = 2; + } else { + // This and the next control fit on the same line. Skip placing the next child. + i++; + } + } + } + + // Measure all children, assuming they all have to fit within the width of the layout. + // Height is unconstrained. + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + ControlLayoutParams lp = getControlLayoutParams(child); + int spec = lp.columnsRequired == 1 ? exactlyColumnWidthSpec : exactlyFullWidthSpec; + measureChild(child, spec, unspecifiedSpec); + } + + // Pack all the children as tightly into rows as possible without changing their ordering. + int layoutHeight = 0; + int nextChildStart = 0; + int nextChildTop = 0; + int currentRowHeight = 0; + int columnsAvailable = 2; + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + ControlLayoutParams lp = getControlLayoutParams(child); + + // If there isn't enough room left for the control, move to the next row. + if (columnsAvailable < lp.columnsRequired) { + layoutHeight += currentRowHeight + mMarginBetweenRows; + nextChildStart = 0; + nextChildTop = layoutHeight; + currentRowHeight = 0; + columnsAvailable = 2; + } + + lp.top = nextChildTop; + lp.start = nextChildStart; + currentRowHeight = Math.max(currentRowHeight, child.getMeasuredHeight()); + columnsAvailable -= lp.columnsRequired; + nextChildStart += lp.columnsRequired * (columnWidth + mMarginBetweenColumns); + } + + // Compute the ViewGroup's height, accounting for the final row's height. + layoutHeight += currentRowHeight; + setMeasuredDimension(resolveSize(fullWidth, widthMeasureSpec), + resolveSize(layoutHeight, heightMeasureSpec)); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int width = right - left; + boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; + + // Child positions were already determined during the measurement pass. + for (int childIndex = 0; childIndex < getChildCount(); childIndex++) { + View child = getChildAt(childIndex); + int childLeft = getControlLayoutParams(child).start; + if (isRtl) childLeft = width - childLeft - child.getMeasuredWidth(); + + int childTop = getControlLayoutParams(child).top; + int childRight = childLeft + child.getMeasuredWidth(); + int childBottom = childTop + child.getMeasuredHeight(); + child.layout(childLeft, childTop, childRight, childBottom); + } + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new ControlLayoutParams(); + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return generateDefaultLayoutParams(); + } + + @Override + protected LayoutParams generateLayoutParams(LayoutParams p) { + return generateDefaultLayoutParams(); + } + + /** + * Adds an icon with a descriptive message to the Title. + * + * ----------------------------------------------------- + * | ICON | TITLE MESSAGE | + * ----------------------------------------------------- + * If an icon is not provided, the ImageView that would normally show it is hidden. + * + * @param iconResourceId ID of the drawable to use for the icon. + * @param titleMessage Message to display on Infobar title. + */ + public View addIconTitle(int iconResourceId, CharSequence titleMessage) { + LinearLayout layout = (LinearLayout) inflateLayout( + getContext(), R.layout.infobar_control_icon_with_description, this); + addView(layout, new ControlLayoutParams()); + + ImageView iconView = (ImageView) layout.findViewById(R.id.control_icon); + iconView.setImageResource(iconResourceId); + + TextView titleView = (TextView) layout.findViewById(R.id.control_message); + titleView.setText(titleMessage); + titleView.setTextSize(TypedValue.COMPLEX_UNIT_PX, + getContext().getResources().getDimension(R.dimen.infobar_text_size)); + + return layout; + } + + /** + * Adds an icon with a descriptive message to the layout. + * + * ----------------------------------------------------- + * | ICON | PRIMARY MESSAGE SECONDARY MESSAGE | + * ----------------------------------------------------- + * If an icon is not provided, the ImageView that would normally show it is hidden. + * + * @param iconResourceId ID of the drawable to use for the icon. + * @param iconColorId ID of the tint color for the icon, or 0 for default. + * @param primaryMessage Message to display for the toggle. + * @param secondaryMessage Additional descriptive text for the toggle. May be null. + */ + public View addIcon(int iconResourceId, int iconColorId, CharSequence primaryMessage, + CharSequence secondaryMessage) { + return addIcon(iconResourceId, iconColorId, primaryMessage, secondaryMessage, + R.dimen.infobar_text_size); + } + + /** + * Adds an icon with a descriptive message to the layout. + * + * ----------------------------------------------------- + * | ICON | PRIMARY MESSAGE SECONDARY MESSAGE | + * ----------------------------------------------------- + * If an icon is not provided, the ImageView that would normally show it is hidden. + * + * @param iconResourceId ID of the drawable to use for the icon. + * @param iconColorId ID of the tint color for the icon, or 0 for default. + * @param primaryMessage Message to display for the toggle. + * @param secondaryMessage Additional descriptive text for the toggle. May be null. + * @param resourceId Size of resource id to be applied to primaryMessage + * and secondaryMessage. + */ + public View addIcon(int iconResourceId, int iconColorId, CharSequence primaryMessage, + CharSequence secondaryMessage, int resourceId) { + LinearLayout layout = (LinearLayout) inflateLayout( + getContext(), R.layout.infobar_control_icon_with_description, this); + addView(layout, new ControlLayoutParams()); + + ImageView iconView = (ImageView) layout.findViewById(R.id.control_icon); + iconView.setImageResource(iconResourceId); + if (iconColorId != 0) { + iconView.setColorFilter(ApiCompatibilityUtils.getColor(getResources(), iconColorId)); + } + + // The primary message text is always displayed. + TextView primaryView = (TextView) layout.findViewById(R.id.control_message); + primaryView.setText(primaryMessage); + primaryView.setTextSize( + TypedValue.COMPLEX_UNIT_PX, getContext().getResources().getDimension(resourceId)); + + // The secondary message text is optional. + TextView secondaryView = (TextView) layout.findViewById(R.id.control_secondary_message); + if (secondaryMessage == null) { + layout.removeView(secondaryView); + } else { + secondaryView.setText(secondaryMessage); + secondaryView.setTextSize(TypedValue.COMPLEX_UNIT_PX, + getContext().getResources().getDimension(resourceId)); + } + + return layout; + } + + /** + * Creates a standard toggle switch and adds it to the layout. + * + * ------------------------------------------------- + * | ICON | MESSAGE | TOGGLE | + * ------------------------------------------------- + * If an icon is not provided, the ImageView that would normally show it is hidden. + * + * @param iconResourceId ID of the drawable to use for the icon, or 0 to hide the ImageView. + * @param iconColorId ID of the tint color for the icon, or 0 for default. + * @param toggleMessage Message to display for the toggle. + * @param toggleId ID to use for the toggle. + * @param isChecked Whether the toggle should start off checked. + */ + public View addSwitch(int iconResourceId, int iconColorId, CharSequence toggleMessage, + int toggleId, boolean isChecked) { + LinearLayout switchLayout = + (LinearLayout) inflateLayout(getContext(), R.layout.infobar_control_toggle, this); + addView(switchLayout, new ControlLayoutParams()); + + ImageView iconView = (ImageView) switchLayout.findViewById(R.id.control_icon); + if (iconResourceId == 0) { + switchLayout.removeView(iconView); + } else { + iconView.setImageResource(iconResourceId); + if (iconColorId != 0) { + iconView.setColorFilter( + ApiCompatibilityUtils.getColor(getResources(), iconColorId)); + } + } + + TextView messageView = (TextView) switchLayout.findViewById(R.id.control_message); + messageView.setText(toggleMessage); + + SwitchCompat switchView = + (SwitchCompat) switchLayout.findViewById(R.id.control_toggle_switch); + switchView.setId(toggleId); + switchView.setChecked(isChecked); + + return switchLayout; + } + + /** + * Creates a set of standard radio buttons and adds it to the layout. + * + * @param messages Messages to display for the options. + * @param tags Optional list of tags to attach to the buttons. + */ + public RadioButtonLayout addRadioButtons(List<CharSequence> messages, @Nullable List<?> tags) { + ControlLayoutParams params = new ControlLayoutParams(); + params.mMustBeFullWidth = true; + + RadioButtonLayout radioLayout = new RadioButtonLayout(getContext()); + radioLayout.addOptions(messages, tags); + + addView(radioLayout, params); + return radioLayout; + } + + /** + * Creates a standard spinner and adds it to the layout. + */ + public <T> Spinner addSpinner(int spinnerId, ArrayAdapter<T> arrayAdapter) { + Spinner spinner = + (Spinner) inflateLayout(getContext(), R.layout.infobar_control_spinner, this); + spinner.setAdapter(arrayAdapter); + addView(spinner, new ControlLayoutParams()); + spinner.setId(spinnerId); + return spinner; + } + + /** + * Creates and adds a full-width control with additional text describing what an InfoBar is for. + */ + public View addDescription(CharSequence message) { + ControlLayoutParams params = new ControlLayoutParams(); + params.mMustBeFullWidth = true; + + TextView descriptionView = + (TextView) inflateLayout(getContext(), R.layout.dialog_control_description, this); + addView(descriptionView, params); + + descriptionView.setText(message); + descriptionView.setMovementMethod(LinkMovementMethod.getInstance()); + return descriptionView; + } + + /** + * Do NOT call this method directly from outside {@link InfoBarLayout#InfoBarLayout()}. + * + * Adds a full-width control showing the main InfoBar message. For other text, you should call + * {@link InfoBarControlLayout#addDescription(CharSequence)} instead. + */ + TextView addMainMessage(CharSequence mainMessage) { + ControlLayoutParams params = new ControlLayoutParams(); + params.mMustBeFullWidth = true; + + TextView messageView = + (TextView) inflateLayout(getContext(), R.layout.infobar_control_message, this); + addView(messageView, params); + + messageView.setText(mainMessage); + messageView.setMovementMethod(LinkMovementMethod.getInstance()); + return messageView; + } + + /** + * @return The {@link ControlLayoutParams} for the given child. + */ + @VisibleForTesting + static ControlLayoutParams getControlLayoutParams(View child) { + return (ControlLayoutParams) child.getLayoutParams(); + } + + private static View inflateLayout(Context context, int layoutId, ViewGroup root) { + // LayoutInflater may trigger accessing the disk. + try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) { + return LayoutInflater.from(context).inflate(layoutId, root, false); + } + } +} diff --git a/chromium/components/infobars/android/java/src/org/chromium/components/infobars/InfoBarControlLayoutTest.java b/chromium/components/infobars/android/java/src/org/chromium/components/infobars/InfoBarControlLayoutTest.java new file mode 100644 index 00000000000..e5925857b33 --- /dev/null +++ b/chromium/components/infobars/android/java/src/org/chromium/components/infobars/InfoBarControlLayoutTest.java @@ -0,0 +1,187 @@ +// Copyright 2015 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.components.infobars; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.annotation.UiThreadTest; +import android.support.test.rule.UiThreadTestRule; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup.LayoutParams; + +import androidx.test.filters.SmallTest; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.test.BaseJUnit4ClassRunner; +import org.chromium.components.infobars.InfoBarControlLayout.ControlLayoutParams; + +/** + * Tests for InfoBarControlLayout. This suite doesn't check for specific details, like margins + * paddings, and instead focuses on whether controls are placed correctly. + */ +@RunWith(BaseJUnit4ClassRunner.class) +public class InfoBarControlLayoutTest { + private static final int SWITCH_ID_1 = 1; + private static final int SWITCH_ID_2 = 2; + private static final int SWITCH_ID_3 = 3; + private static final int SWITCH_ID_4 = 4; + private static final int SWITCH_ID_5 = 5; + private static final int INFOBAR_WIDTH = 3200; + + private Context mContext; + + @Rule + public UiThreadTestRule mRule = new UiThreadTestRule(); + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getTargetContext(); + mContext.setTheme(R.style.Theme_BrowserUI); + } + + /** + * A small control on the last line takes up the full width. + */ + @Test + @SmallTest + @UiThreadTest + public void testOneSmallControlTakesFullWidth() { + InfoBarControlLayout layout = new InfoBarControlLayout(mContext); + layout.setLayoutParams( + new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + View smallSwitch = layout.addSwitch(0, 0, "A", SWITCH_ID_1, false); + + // Trigger the measurement algorithm. + int parentWidthSpec = MeasureSpec.makeMeasureSpec(INFOBAR_WIDTH, MeasureSpec.AT_MOST); + int parentHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + layout.measure(parentWidthSpec, parentHeightSpec); + + // Small control takes the full width of the layout because it's put on its own line. + ControlLayoutParams params = InfoBarControlLayout.getControlLayoutParams(smallSwitch); + Assert.assertEquals(0, params.top); + Assert.assertEquals(0, params.start); + Assert.assertEquals(2, params.columnsRequired); + Assert.assertEquals(INFOBAR_WIDTH, smallSwitch.getMeasuredWidth()); + } + + /** + * Tests the layout algorithm on a set of five controls, the second of which is a huge control + * and takes up the whole line. The other smaller controls try to pack themselves as tightly + * as possible, strecthing out if necessary for aesthetics, resulting in a layout like this: + * + * ------------------------- + * | A (small) | + * ------------------------- + * | B (big) | + * ------------------------- + * | C (small) | D (small) | + * ------------------------- + * | E (small) | + * ------------------------- + */ + @Test + @SmallTest + @UiThreadTest + public void testComplexSwitchLayout() { + // Add five controls to the layout. + InfoBarControlLayout layout = new InfoBarControlLayout(mContext); + layout.setLayoutParams( + new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + + View switch1 = layout.addSwitch(0, 0, "A", SWITCH_ID_1, false); + View switch2 = layout.addSwitch(0, 0, "B", SWITCH_ID_2, false); + View switch3 = layout.addSwitch(0, 0, "C", SWITCH_ID_3, false); + View switch4 = layout.addSwitch(0, 0, "D", SWITCH_ID_4, false); + View switch5 = layout.addSwitch(0, 0, "E", SWITCH_ID_4, false); + + // Make the second control require the full layout width. + switch2.setMinimumWidth(INFOBAR_WIDTH); + + // Trigger the measurement algorithm. + int parentWidthSpec = MeasureSpec.makeMeasureSpec(INFOBAR_WIDTH, MeasureSpec.AT_MOST); + int parentHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + layout.measure(parentWidthSpec, parentHeightSpec); + + ControlLayoutParams params1 = InfoBarControlLayout.getControlLayoutParams(switch1); + ControlLayoutParams params2 = InfoBarControlLayout.getControlLayoutParams(switch2); + ControlLayoutParams params3 = InfoBarControlLayout.getControlLayoutParams(switch3); + ControlLayoutParams params4 = InfoBarControlLayout.getControlLayoutParams(switch4); + ControlLayoutParams params5 = InfoBarControlLayout.getControlLayoutParams(switch5); + + // Small control takes the full width of the layout because the next one doesn't fit. + Assert.assertEquals(0, params1.top); + Assert.assertEquals(0, params1.start); + Assert.assertEquals(2, params1.columnsRequired); + Assert.assertEquals(INFOBAR_WIDTH, switch1.getMeasuredWidth()); + + // Big control gets shunted onto the next row and takes up the whole space. + Assert.assertTrue(params2.top > switch1.getMeasuredHeight()); + Assert.assertEquals(0, params2.start); + Assert.assertEquals(2, params2.columnsRequired); + Assert.assertEquals(INFOBAR_WIDTH, switch2.getMeasuredWidth()); + + // Small control gets placed onto the next line and takes only half the width. + int bottomOfSwitch2 = params2.top + switch2.getMeasuredHeight(); + Assert.assertTrue(params3.top > bottomOfSwitch2); + Assert.assertEquals(0, params3.start); + Assert.assertEquals(1, params3.columnsRequired); + Assert.assertTrue(switch3.getMeasuredWidth() < INFOBAR_WIDTH); + + // Small control gets placed next to the previous small control. + Assert.assertEquals(params3.top, params4.top); + Assert.assertTrue(params4.start > switch3.getMeasuredWidth()); + Assert.assertEquals(1, params4.columnsRequired); + Assert.assertTrue(switch4.getMeasuredWidth() < INFOBAR_WIDTH); + + // Last small control has no room left and gets put on its own line, taking the full width. + int bottomOfSwitch4 = params4.top + switch4.getMeasuredHeight(); + Assert.assertTrue(params5.top > bottomOfSwitch4); + Assert.assertEquals(0, params5.start); + Assert.assertEquals(2, params5.columnsRequired); + Assert.assertEquals(INFOBAR_WIDTH, switch5.getMeasuredWidth()); + } + + /** + * Tests that the message is always the full width of the layout. + */ + @Test + @SmallTest + @UiThreadTest + public void testFullWidthMessageControl() { + // Add two controls to the layout. The main message automatically requests the full width. + InfoBarControlLayout layout = new InfoBarControlLayout(mContext); + layout.setLayoutParams( + new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + + View view1 = layout.addMainMessage("A"); + View view2 = layout.addSwitch(0, 0, "B", SWITCH_ID_2, false); + + // Trigger the measurement algorithm. + int parentWidthSpec = MeasureSpec.makeMeasureSpec(INFOBAR_WIDTH, MeasureSpec.AT_MOST); + int parentHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + layout.measure(parentWidthSpec, parentHeightSpec); + + ControlLayoutParams params1 = InfoBarControlLayout.getControlLayoutParams(view1); + ControlLayoutParams params2 = InfoBarControlLayout.getControlLayoutParams(view2); + + // Main message takes up the full space. + Assert.assertEquals(0, params1.top); + Assert.assertEquals(0, params1.start); + Assert.assertEquals(2, params1.columnsRequired); + Assert.assertEquals(INFOBAR_WIDTH, view1.getMeasuredWidth()); + + // Small control gets shunted onto the next row. + Assert.assertTrue(params2.top > view1.getMeasuredHeight()); + Assert.assertEquals(0, params2.start); + Assert.assertEquals(2, params2.columnsRequired); + Assert.assertEquals(INFOBAR_WIDTH, view2.getMeasuredWidth()); + } +} diff --git a/chromium/components/infobars/android/java/src/org/chromium/components/infobars/InfoBarInteractionHandler.java b/chromium/components/infobars/android/java/src/org/chromium/components/infobars/InfoBarInteractionHandler.java new file mode 100644 index 00000000000..f0f84877429 --- /dev/null +++ b/chromium/components/infobars/android/java/src/org/chromium/components/infobars/InfoBarInteractionHandler.java @@ -0,0 +1,31 @@ +// Copyright 2013 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.components.infobars; + +/** + * Functions needed to display an InfoBar UI. + */ +public interface InfoBarInteractionHandler { + /** + * Handles click on the infobar. It is invoked before one of the following functions. + */ + public void onClick(); + + /** + * Takes some action related to the link being clicked. + */ + public void onLinkClicked(); + + /** + * Takes some action related to the close button being clicked. + */ + public void onCloseButtonClicked(); + + /** + * Performs some action related to either the primary or secondary button being pressed. + * @param isPrimaryButton True if the primary button was clicked, false otherwise. + */ + public void onButtonClicked(boolean isPrimaryButton); +} diff --git a/chromium/components/infobars/android/java/src/org/chromium/components/infobars/InfoBarLayout.java b/chromium/components/infobars/android/java/src/org/chromium/components/infobars/InfoBarLayout.java new file mode 100644 index 00000000000..d681631a84b --- /dev/null +++ b/chromium/components/infobars/android/java/src/org/chromium/components/infobars/InfoBarLayout.java @@ -0,0 +1,573 @@ +// Copyright 2013 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.components.infobars; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.ColorRes; +import androidx.annotation.Nullable; +import androidx.appcompat.content.res.AppCompatResources; + +import org.chromium.base.ApiCompatibilityUtils; +import org.chromium.components.browser_ui.widget.DualControlLayout; +import org.chromium.ui.UiUtils; +import org.chromium.ui.text.NoUnderlineClickableSpan; +import org.chromium.ui.widget.ButtonCompat; +import org.chromium.ui.widget.ChromeImageButton; +import org.chromium.ui.widget.ChromeImageView; + +import java.util.ArrayList; +import java.util.List; + +/** + * Layout that arranges an infobar's views. + * + * An InfoBarLayout consists of: + * - A message describing why the infobar is being displayed. + * - A close button in the top right corner. + * - (optional) An icon representing the infobar's purpose in the top left corner. + * - (optional) Additional {@link InfoBarControlLayouts} for specialized controls (e.g. spinners). + * - (optional) One or two buttons with text at the bottom, or a button paired with an ImageView. + * + * When adding custom views, widths and heights defined in the LayoutParams will be ignored. + * Setting a minimum width using {@link View#setMinimumWidth()} will be obeyed. + * + * Logic for what happens when things are clicked should be implemented by the + * InfoBarInteractionHandler. + */ +public final class InfoBarLayout extends ViewGroup implements View.OnClickListener { + /** + * Parameters used for laying out children. + */ + private static class LayoutParams extends ViewGroup.LayoutParams { + public int startMargin; + public int endMargin; + public int topMargin; + public int bottomMargin; + + // Where this view will be laid out. Calculated in onMeasure() and used in onLayout(). + public int start; + public int top; + + LayoutParams(int startMargin, int topMargin, int endMargin, int bottomMargin) { + super(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + this.startMargin = startMargin; + this.topMargin = topMargin; + this.endMargin = endMargin; + this.bottomMargin = bottomMargin; + } + } + + private final int mSmallIconSize; + private final int mSmallIconMargin; + private final int mBigIconSize; + private final int mBigIconMargin; + private final int mMarginAboveButtonGroup; + private final int mMarginAboveControlGroups; + private final int mPadding; + private final int mMinWidth; + + private final InfoBarInteractionHandler mInfoBar; + private final ImageButton mCloseButton; + private final InfoBarControlLayout mMessageLayout; + private final List<InfoBarControlLayout> mControlLayouts; + + private TextView mMessageTextView; + private ImageView mIconView; + private DualControlLayout mButtonRowLayout; + + private CharSequence mMessageMainText; + private String mMessageLinkText; + private int mMessageInlineLinkRangeStart; + private int mMessageInlineLinkRangeEnd; + + /** + * Constructs a layout for the specified infobar. After calling this, be sure to set the + * message, the buttons, and/or the custom content using setMessage(), setButtons(), and + * setCustomContent(). + * @param context The context used to render. + * @param infoBar InfoBarInteractionHandler that listens to events. + * @param iconResourceId ID of the icon to use for the infobar. + * @param iconTintId The {@link ColorRes} used as tint for {@code iconResourceId}. + * @param iconBitmap Bitmap for the icon to use, if the resource ID wasn't passed through. + * @param message The message to show in the infobar. + */ + public InfoBarLayout(Context context, InfoBarInteractionHandler infoBar, int iconResourceId, + @ColorRes int iconTintId, Bitmap iconBitmap, CharSequence message) { + super(context); + mControlLayouts = new ArrayList<InfoBarControlLayout>(); + + mInfoBar = infoBar; + + // Cache resource values. + Resources res = getResources(); + mSmallIconSize = res.getDimensionPixelSize(R.dimen.infobar_small_icon_size); + mSmallIconMargin = res.getDimensionPixelSize(R.dimen.infobar_small_icon_margin); + mBigIconSize = res.getDimensionPixelSize(R.dimen.infobar_big_icon_size); + mBigIconMargin = res.getDimensionPixelSize(R.dimen.infobar_big_icon_margin); + mMarginAboveButtonGroup = + res.getDimensionPixelSize(R.dimen.infobar_margin_above_button_row); + mMarginAboveControlGroups = + res.getDimensionPixelSize(R.dimen.infobar_margin_above_control_groups); + mPadding = res.getDimensionPixelOffset(R.dimen.infobar_padding); + mMinWidth = res.getDimensionPixelSize(R.dimen.infobar_min_width); + + // Set up the close button. Apply padding so it has a big touch target. + mCloseButton = createCloseButton(context); + mCloseButton.setOnClickListener(this); + mCloseButton.setPadding(mPadding, mPadding, mPadding, mPadding); + mCloseButton.setLayoutParams(new LayoutParams(0, -mPadding, -mPadding, -mPadding)); + + // Set up the icon, if necessary. + mIconView = createIconView(context, iconResourceId, iconTintId, iconBitmap); + if (mIconView != null) { + mIconView.setLayoutParams(new LayoutParams(0, 0, mSmallIconMargin, 0)); + mIconView.getLayoutParams().width = mSmallIconSize; + mIconView.getLayoutParams().height = mSmallIconSize; + } + + // Set up the message view. + mMessageMainText = message; + mMessageLayout = new InfoBarControlLayout(context); + mMessageTextView = mMessageLayout.addMainMessage(prepareMainMessageString()); + } + + /** + * Returns the {@link TextView} corresponding to the main infobar message. + * The returned view is a part of internal layout strucutre and shouldn't be accessed by InfoBar + * implementations. + */ + public TextView getMessageTextView() { + return mMessageTextView; + } + + /** + * Returns the {@link InfoBarControlLayout} containing the TextView showing the main infobar + * message and associated controls, which is sandwiched between its icon and close button. + * The returned view is a part of internal layout strucutre and shouldn't be accessed by InfoBar + * implementations. + */ + public InfoBarControlLayout getMessageLayout() { + return mMessageLayout; + } + + /** + * Sets the message to show on the infobar. + * TODO(dfalcantara): Do some magic here to determine if TextViews need to have line spacing + * manually added. Android changed when these values were applied between + * KK and L: https://crbug.com/543205 + */ + public void setMessage(CharSequence message) { + mMessageMainText = message; + mMessageTextView.setText(prepareMainMessageString()); + } + + /** + * Appends a link to the message, if an infobar requires one (e.g. "Learn more"). + */ + public void appendMessageLinkText(String linkText) { + mMessageLinkText = linkText; + mMessageTextView.setText(prepareMainMessageString()); + } + + /** + * Sets up the message to have an inline link, assuming an inclusive range. + * @param rangeStart Where the link starts. + * @param rangeEnd Where the link ends. + */ + public void setInlineMessageLink(int rangeStart, int rangeEnd) { + mMessageInlineLinkRangeStart = rangeStart; + mMessageInlineLinkRangeEnd = rangeEnd; + mMessageTextView.setText(prepareMainMessageString()); + } + + /** + * Adds an {@link InfoBarControlLayout} to house additional infobar controls, like toggles and + * spinners. + */ + public InfoBarControlLayout addControlLayout() { + InfoBarControlLayout controlLayout = new InfoBarControlLayout(getContext()); + mControlLayouts.add(controlLayout); + return controlLayout; + } + + /** + * Adds one or two buttons to the layout. + * + * @param primaryText Text for the primary button. If empty, no buttons are added at all. + * @param secondaryText Text for the secondary button, or null if there isn't a second button. + */ + public void setButtons(String primaryText, String secondaryText) { + if (TextUtils.isEmpty(primaryText)) { + assert TextUtils.isEmpty(secondaryText); + return; + } + + Button secondaryButton = null; + if (!TextUtils.isEmpty(secondaryText)) { + secondaryButton = DualControlLayout.createButtonForLayout( + getContext(), false, secondaryText, this); + } + + setBottomViews( + primaryText, secondaryButton, DualControlLayout.DualControlLayoutAlignment.END); + } + + /** + * Sets up the bottom-most part of the infobar with a primary button (e.g. OK) and a secondary + * View of your choice. Subclasses should be calling {@link #setButtons(String, String)} + * instead of this function in nearly all cases (that function calls this one). + * + * @param primaryText Text to display on the primary button. If empty, the bottom layout is not + * created. + * @param secondaryView View that is aligned with the primary button. May be null. + * @param alignment One of ALIGN_START, ALIGN_APART, or ALIGN_END from + * {@link DualControlLayout}. + */ + public void setBottomViews(String primaryText, View secondaryView, int alignment) { + assert !TextUtils.isEmpty(primaryText); + Button primaryButton = + DualControlLayout.createButtonForLayout(getContext(), true, primaryText, this); + + assert mButtonRowLayout == null; + mButtonRowLayout = new DualControlLayout(getContext(), null); + mButtonRowLayout.setAlignment(alignment); + mButtonRowLayout.setStackedMargin(getResources().getDimensionPixelSize( + R.dimen.infobar_margin_between_stacked_buttons)); + + mButtonRowLayout.addView(primaryButton); + if (secondaryView != null) mButtonRowLayout.addView(secondaryView); + } + + /** + * Adjusts styling to account for the big icon layout. + */ + public void setIsUsingBigIcon() { + if (mIconView == null) return; + + LayoutParams lp = (LayoutParams) mIconView.getLayoutParams(); + lp.width = mBigIconSize; + lp.height = mBigIconSize; + lp.endMargin = mBigIconMargin; + + Resources res = getContext().getResources(); + float textSize = res.getDimension(R.dimen.infobar_big_icon_message_size); + mMessageTextView.setTypeface(UiUtils.createRobotoMediumTypeface()); + mMessageTextView.setMaxLines(1); + mMessageTextView.setEllipsize(TextUtils.TruncateAt.END); + mMessageTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); + } + + /** + * Returns the primary button, or null if it doesn't exist. + */ + public ButtonCompat getPrimaryButton() { + return mButtonRowLayout == null + ? null + : (ButtonCompat) mButtonRowLayout.findViewById(R.id.button_primary); + } + + /** + * Returns the icon, or null if it doesn't exist. + */ + public ImageView getIcon() { + return mIconView; + } + + /** + * Must be called after the message, buttons, and custom content have been set, and before the + * first call to onMeasure(). + */ + // TODO(crbug/1056346): onContentCreated is made public to allow access from InfoBar. Once + // InfoBar is modularized, restore access to package private. + public void onContentCreated() { + // Add the child views in the desired focus order. + if (mIconView != null) addView(mIconView); + addView(mMessageLayout); + for (View v : mControlLayouts) addView(v); + if (mButtonRowLayout != null) addView(mButtonRowLayout); + addView(mCloseButton); + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(0, 0, 0, 0); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + // Place all the views in the positions already determined during onMeasure(). + int width = right - left; + boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; + + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + int childLeft = lp.start; + int childRight = lp.start + child.getMeasuredWidth(); + + if (isRtl) { + int tmp = width - childRight; + childRight = width - childLeft; + childLeft = tmp; + } + + child.layout(childLeft, lp.top, childRight, lp.top + child.getMeasuredHeight()); + } + } + + /** + * Measures and determines where children should go. + * + * For current specs, see https://goto.google.com/infobar-spec + * + * All controls are padded from the infobar boundary by the same amount, but different types of + * control groups are bound by different widths and have different margins: + * -------------------------------------------------------------------------------- + * | PADDING | + * | -------------------------------------------------------------------------- | + * | | ICON | MESSAGE LAYOUT | X | | + * | |------+ +---| | + * | | | | | | + * | | ------------------------------------------------------------------| | + * | | | CONTROL LAYOUT #1 | | + * | | ------------------------------------------------------------------| | + * | | | CONTROL LAYOUT #X | | + * | |------------------------------------------------------------------------| | + * | | BOTTOM ROW LAYOUT | | + * | -------------------------------------------------------------------------| | + * | | + * -------------------------------------------------------------------------------- + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + assert getLayoutParams().height + == LayoutParams.WRAP_CONTENT : "InfoBar heights cannot be constrained."; + + // Apply the padding that surrounds all the infobar controls. + final int layoutWidth = Math.max(MeasureSpec.getSize(widthMeasureSpec), mMinWidth); + final int paddedStart = mPadding; + final int paddedEnd = layoutWidth - mPadding; + int layoutBottom = mPadding; + + // Measure and place the icon in the top-left corner. + int unspecifiedSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + if (mIconView != null) { + LayoutParams iconParams = getChildLayoutParams(mIconView); + measureChild(mIconView, unspecifiedSpec, unspecifiedSpec); + iconParams.start = paddedStart + iconParams.startMargin; + iconParams.top = layoutBottom + iconParams.topMargin; + } + final int iconWidth = getChildWidthWithMargins(mIconView); + + // Measure and place the close button in the top-right corner of the layout. + LayoutParams closeParams = getChildLayoutParams(mCloseButton); + measureChild(mCloseButton, unspecifiedSpec, unspecifiedSpec); + closeParams.start = paddedEnd - closeParams.endMargin - mCloseButton.getMeasuredWidth(); + closeParams.top = layoutBottom + closeParams.topMargin; + + // Determine how much width is available for all the different control layouts; see the + // function JavaDoc above for details. + final int paddedWidth = paddedEnd - paddedStart; + final int controlLayoutWidth = paddedWidth - iconWidth; + final int messageWidth = controlLayoutWidth - getChildWidthWithMargins(mCloseButton); + + // The message layout is sandwiched between the icon and the close button. + LayoutParams messageParams = getChildLayoutParams(mMessageLayout); + measureChildWithFixedWidth(mMessageLayout, messageWidth); + messageParams.start = paddedStart + iconWidth; + messageParams.top = layoutBottom; + + // Control layouts are placed below the message layout and the close button. The icon is + // ignored for this particular calculation because the icon enforces a left margin on all of + // the control layouts and won't be overlapped. + layoutBottom += Math.max( + getChildHeightWithMargins(mMessageLayout), getChildHeightWithMargins(mCloseButton)); + + // The other control layouts are constrained only by the icon's width. + final int controlPaddedStart = paddedStart + iconWidth; + for (int i = 0; i < mControlLayouts.size(); i++) { + View child = mControlLayouts.get(i); + measureChildWithFixedWidth(child, controlLayoutWidth); + + layoutBottom += mMarginAboveControlGroups; + getChildLayoutParams(child).start = controlPaddedStart; + getChildLayoutParams(child).top = layoutBottom; + layoutBottom += child.getMeasuredHeight(); + } + + // The button layout takes up the full width of the infobar and sits below everything else, + // including the icon. + layoutBottom = Math.max(layoutBottom, getChildHeightWithMargins(mIconView)); + if (mButtonRowLayout != null) { + measureChildWithFixedWidth(mButtonRowLayout, paddedWidth); + + layoutBottom += mMarginAboveButtonGroup; + getChildLayoutParams(mButtonRowLayout).start = paddedStart; + getChildLayoutParams(mButtonRowLayout).top = layoutBottom; + layoutBottom += mButtonRowLayout.getMeasuredHeight(); + } + + // Apply padding to the bottom of the infobar. + layoutBottom += mPadding; + + setMeasuredDimension(resolveSize(layoutWidth, widthMeasureSpec), + resolveSize(layoutBottom, heightMeasureSpec)); + } + + private static int getChildWidthWithMargins(View view) { + if (view == null) return 0; + return view.getMeasuredWidth() + getChildLayoutParams(view).startMargin + + getChildLayoutParams(view).endMargin; + } + + private static int getChildHeightWithMargins(View view) { + if (view == null) return 0; + return view.getMeasuredHeight() + getChildLayoutParams(view).topMargin + + getChildLayoutParams(view).bottomMargin; + } + + private static LayoutParams getChildLayoutParams(View view) { + return (LayoutParams) view.getLayoutParams(); + } + + /** + * Measures a child for the given space, taking into account its margins. + */ + private void measureChildWithFixedWidth(View child, int width) { + LayoutParams lp = getChildLayoutParams(child); + int availableWidth = width - lp.startMargin - lp.endMargin; + int widthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.EXACTLY); + int heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + child.measure(widthSpec, heightSpec); + } + + /** + * Listens for View clicks. + * Classes that override this function MUST call this one. + * @param view View that was clicked on. + */ + @Override + public void onClick(View view) { + mInfoBar.onClick(); + + if (view.getId() == R.id.infobar_close_button) { + mInfoBar.onCloseButtonClicked(); + } else if (view.getId() == R.id.button_primary) { + mInfoBar.onButtonClicked(true); + } else if (view.getId() == R.id.button_secondary) { + mInfoBar.onButtonClicked(false); + } + } + + /** + * Prepares text to be displayed as the infobar's main message, including setting up a + * clickable link if the infobar requires it. + */ + private CharSequence prepareMainMessageString() { + SpannableStringBuilder fullString = new SpannableStringBuilder(); + + if (!TextUtils.isEmpty(mMessageMainText)) { + SpannableString spannedMessage = new SpannableString(mMessageMainText); + + // If there's an inline link, apply the necessary span for it. + if (mMessageInlineLinkRangeEnd != 0) { + assert mMessageInlineLinkRangeStart < mMessageInlineLinkRangeEnd; + assert mMessageInlineLinkRangeEnd < mMessageMainText.length(); + + spannedMessage.setSpan(createClickableSpan(), mMessageInlineLinkRangeStart, + mMessageInlineLinkRangeEnd, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } + + fullString.append(spannedMessage); + } + + // Concatenate the text to display for the link and make it clickable. + if (!TextUtils.isEmpty(mMessageLinkText)) { + if (fullString.length() > 0) fullString.append(" "); + int spanStart = fullString.length(); + + fullString.append(mMessageLinkText); + fullString.setSpan(createClickableSpan(), spanStart, fullString.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + return fullString; + } + + private NoUnderlineClickableSpan createClickableSpan() { + return new NoUnderlineClickableSpan(getResources(), (view) -> mInfoBar.onLinkClicked()); + } + + /** + * Creates a View that holds an icon representing an infobar. + * @param context Context to grab resources from. + * @param iconResourceId ID of the icon to use for the infobar. + * @param iconTintId The {@link ColorRes} used as tint for {@code iconResourceId}. + * @param iconBitmap Bitmap for the icon to use, if the resource ID wasn't passed through. + * @return {@link ImageButton} that represents the icon. + */ + @Nullable + public static ImageView createIconView( + Context context, int iconResourceId, @ColorRes int iconTintId, Bitmap iconBitmap) { + if (iconResourceId == 0 && iconBitmap == null) return null; + + final ChromeImageView iconView = new ChromeImageView(context); + if (iconResourceId != 0) { + iconView.setImageDrawable(AppCompatResources.getDrawable(context, iconResourceId)); + if (iconTintId != 0) { + ApiCompatibilityUtils.setImageTintList( + iconView, AppCompatResources.getColorStateList(context, iconTintId)); + } + } else { + iconView.setImageBitmap(iconBitmap); + } + + iconView.setFocusable(false); + iconView.setId(R.id.infobar_icon); + iconView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + return iconView; + } + + /** + * Creates a close button that can be inserted into an infobar. + * @param context Context to grab resources from. + * @return {@link ImageButton} that represents a close button. + */ + public static ImageButton createCloseButton(Context context) { + final ColorStateList tint = + AppCompatResources.getColorStateList(context, R.color.default_icon_color); + TypedArray a = + context.obtainStyledAttributes(new int[] {android.R.attr.selectableItemBackground}); + Drawable closeButtonBackground = a.getDrawable(0); + a.recycle(); + + ChromeImageButton closeButton = new ChromeImageButton(context); + closeButton.setId(R.id.infobar_close_button); + closeButton.setImageResource(R.drawable.btn_close); + ApiCompatibilityUtils.setImageTintList(closeButton, tint); + closeButton.setBackground(closeButtonBackground); + closeButton.setContentDescription(context.getString(R.string.close)); + closeButton.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + + return closeButton; + } +} diff --git a/chromium/components/infobars/android/java/src/org/chromium/components/infobars/InfoBarMessageView.java b/chromium/components/infobars/android/java/src/org/chromium/components/infobars/InfoBarMessageView.java new file mode 100644 index 00000000000..98a8d7ee7e2 --- /dev/null +++ b/chromium/components/infobars/android/java/src/org/chromium/components/infobars/InfoBarMessageView.java @@ -0,0 +1,53 @@ +// 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.components.infobars; + +import android.content.Context; +import android.text.style.ClickableSpan; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import org.chromium.ui.widget.TextViewWithClickableSpans; + +/** + * Handles the additional message view responsibilities needed for InfoBars. + * - Makes the full text view clickable if there is just a single link. + */ +public class InfoBarMessageView extends TextViewWithClickableSpans { + private boolean mExternalOnClickListenerSet; + private long mMotionEventDownTime; + + public InfoBarMessageView(Context context) { + super(context); + } + + public InfoBarMessageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + boolean retVal = super.onTouchEvent(event); + if (!mExternalOnClickListenerSet && event.getActionMasked() == MotionEvent.ACTION_UP) { + long downDuration = event.getEventTime() - event.getDownTime(); + boolean validClickEvent = downDuration >= ViewConfiguration.getTapTimeout() + && downDuration <= ViewConfiguration.getLongPressTimeout(); + + ClickableSpan[] spans = getClickableSpans(); + if (validClickEvent && spans != null && spans.length == 1 + && !touchIntersectsAnyClickableSpans(event)) { + spans[0].onClick(this); + } + } + return retVal; + } + + @Override + public final void setOnClickListener(OnClickListener l) { + super.setOnClickListener(l); + if (l != null) mExternalOnClickListenerSet = true; + } +} diff --git a/chromium/components/infobars/android/res/drawable-hdpi/infobar_shadow_left.9.png b/chromium/components/infobars/android/res/drawable-hdpi/infobar_shadow_left.9.png Binary files differnew file mode 100644 index 00000000000..d65598923df --- /dev/null +++ b/chromium/components/infobars/android/res/drawable-hdpi/infobar_shadow_left.9.png diff --git a/chromium/components/infobars/android/res/drawable-hdpi/infobar_shadow_top.png b/chromium/components/infobars/android/res/drawable-hdpi/infobar_shadow_top.png Binary files differnew file mode 100644 index 00000000000..2baed48442c --- /dev/null +++ b/chromium/components/infobars/android/res/drawable-hdpi/infobar_shadow_top.png diff --git a/chromium/components/infobars/android/res/drawable-mdpi/infobar_shadow_left.9.png b/chromium/components/infobars/android/res/drawable-mdpi/infobar_shadow_left.9.png Binary files differnew file mode 100644 index 00000000000..59af8e98339 --- /dev/null +++ b/chromium/components/infobars/android/res/drawable-mdpi/infobar_shadow_left.9.png diff --git a/chromium/components/infobars/android/res/drawable-mdpi/infobar_shadow_top.png b/chromium/components/infobars/android/res/drawable-mdpi/infobar_shadow_top.png Binary files differnew file mode 100644 index 00000000000..9dd1e33b27a --- /dev/null +++ b/chromium/components/infobars/android/res/drawable-mdpi/infobar_shadow_top.png diff --git a/chromium/components/infobars/android/res/drawable-xhdpi/infobar_shadow_left.9.png b/chromium/components/infobars/android/res/drawable-xhdpi/infobar_shadow_left.9.png Binary files differnew file mode 100644 index 00000000000..674a728e9a4 --- /dev/null +++ b/chromium/components/infobars/android/res/drawable-xhdpi/infobar_shadow_left.9.png diff --git a/chromium/components/infobars/android/res/drawable-xhdpi/infobar_shadow_top.png b/chromium/components/infobars/android/res/drawable-xhdpi/infobar_shadow_top.png Binary files differnew file mode 100644 index 00000000000..766112897f5 --- /dev/null +++ b/chromium/components/infobars/android/res/drawable-xhdpi/infobar_shadow_top.png diff --git a/chromium/components/infobars/android/res/drawable-xxhdpi/infobar_shadow_left.9.png b/chromium/components/infobars/android/res/drawable-xxhdpi/infobar_shadow_left.9.png Binary files differnew file mode 100644 index 00000000000..4125483c89a --- /dev/null +++ b/chromium/components/infobars/android/res/drawable-xxhdpi/infobar_shadow_left.9.png diff --git a/chromium/components/infobars/android/res/drawable-xxhdpi/infobar_shadow_top.png b/chromium/components/infobars/android/res/drawable-xxhdpi/infobar_shadow_top.png Binary files differnew file mode 100644 index 00000000000..34ae4b52120 --- /dev/null +++ b/chromium/components/infobars/android/res/drawable-xxhdpi/infobar_shadow_top.png diff --git a/chromium/components/infobars/android/res/drawable-xxxhdpi/infobar_shadow_left.9.png b/chromium/components/infobars/android/res/drawable-xxxhdpi/infobar_shadow_left.9.png Binary files differnew file mode 100644 index 00000000000..0f0cc52a2ae --- /dev/null +++ b/chromium/components/infobars/android/res/drawable-xxxhdpi/infobar_shadow_left.9.png diff --git a/chromium/components/infobars/android/res/drawable-xxxhdpi/infobar_shadow_top.png b/chromium/components/infobars/android/res/drawable-xxxhdpi/infobar_shadow_top.png Binary files differnew file mode 100644 index 00000000000..63b926435bc --- /dev/null +++ b/chromium/components/infobars/android/res/drawable-xxxhdpi/infobar_shadow_top.png diff --git a/chromium/components/infobars/android/res/layout/infobar_control_icon_with_description.xml b/chromium/components/infobars/android/res/layout/infobar_control_icon_with_description.xml new file mode 100644 index 00000000000..b9169aa9c33 --- /dev/null +++ b/chromium/components/infobars/android/res/layout/infobar_control_icon_with_description.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2015 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. --> + +<LinearLayout + 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:minHeight="36dp" + android:gravity="center_vertical"> + + <ImageView + android:id="@+id/control_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/dual_control_margin_between_items" + android:scaleType="centerInside" + tools:ignore="ContentDescription" /> + + <org.chromium.components.browser_ui.widget.DualControlLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" > + + <TextView + android:id="@+id/control_message" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.TextLarge.Primary" /> + + <TextView + android:id="@+id/control_secondary_message" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.TextLarge.Secondary" /> + + </org.chromium.components.browser_ui.widget.DualControlLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/chromium/components/infobars/android/res/layout/infobar_control_message.xml b/chromium/components/infobars/android/res/layout/infobar_control_message.xml new file mode 100644 index 00000000000..53fc78601cc --- /dev/null +++ b/chromium/components/infobars/android/res/layout/infobar_control_message.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2015 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. --> + +<org.chromium.components.infobars.InfoBarMessageView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/infobar_message" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textDirection="locale" + android:textAppearance="@style/TextAppearance.TextLarge.Primary" + android:textColorLink="@color/default_text_color_link" /> diff --git a/chromium/components/infobars/android/res/layout/infobar_control_spinner.xml b/chromium/components/infobars/android/res/layout/infobar_control_spinner.xml new file mode 100644 index 00000000000..a03d009323c --- /dev/null +++ b/chromium/components/infobars/android/res/layout/infobar_control_spinner.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2015 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. --> + +<androidx.appcompat.widget.AppCompatSpinner + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="48dp" /> diff --git a/chromium/components/infobars/android/res/layout/infobar_control_spinner_drop_down.xml b/chromium/components/infobars/android/res/layout/infobar_control_spinner_drop_down.xml new file mode 100644 index 00000000000..1b5222c14b9 --- /dev/null +++ b/chromium/components/infobars/android/res/layout/infobar_control_spinner_drop_down.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- Copyright 2016 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:id="@+id/control_spinner_drop_down" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="10dp" + android:layout_marginBottom="10dp" + android:background="?android:attr/selectableItemBackground" + android:ellipsize="end" + android:padding="10dp" + android:singleLine="true" /> diff --git a/chromium/components/infobars/android/res/layout/infobar_control_spinner_view.xml b/chromium/components/infobars/android/res/layout/infobar_control_spinner_view.xml new file mode 100644 index 00000000000..660860aed5c --- /dev/null +++ b/chromium/components/infobars/android/res/layout/infobar_control_spinner_view.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- Copyright 2016 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. +--> + +<org.chromium.components.browser_ui.widget.DualControlLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" > + + <!-- Label for the data. --> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.TextLarge.Secondary" /> + + <!-- Shows the actively selected item. --> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.TextLarge.Primary" /> + +</org.chromium.components.browser_ui.widget.DualControlLayout>
\ No newline at end of file diff --git a/chromium/components/infobars/android/res/layout/infobar_control_toggle.xml b/chromium/components/infobars/android/res/layout/infobar_control_toggle.xml new file mode 100644 index 00000000000..9cd7acaff2c --- /dev/null +++ b/chromium/components/infobars/android/res/layout/infobar_control_toggle.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2015 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. --> + +<LinearLayout + 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:minHeight="36dp" + android:gravity="center_vertical"> + + <ImageView + android:id="@+id/control_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/dual_control_margin_between_items" + android:scaleType="centerInside" + tools:ignore="ContentDescription" /> + + <TextView + android:id="@+id/control_message" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_marginEnd="@dimen/dual_control_margin_between_items" + android:gravity="center_vertical" + android:textAppearance="@style/TextAppearance.TextLarge.Primary" /> + + <androidx.appcompat.widget.SwitchCompat + android:id="@+id/control_toggle_switch" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + +</LinearLayout> diff --git a/chromium/components/infobars/android/res/values/dimens.xml b/chromium/components/infobars/android/res/values/dimens.xml new file mode 100644 index 00000000000..a4c09ffe336 --- /dev/null +++ b/chromium/components/infobars/android/res/values/dimens.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. --> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!-- Infobar dimensions --> + <!-- Maximum width of an infobar. --> + <dimen name="infobar_max_width">600dp</dimen> + <!-- Width of side shadows on floating infobars. --> + <dimen name="infobar_shadow_width">8dp</dimen> + <!-- Height of top shadow on each infobar. --> + <dimen name="infobar_shadow_height">8dp</dimen> + <!-- Distance that a back infobar peeks out above the front infobar. --> + <dimen name="infobar_peeking_height">10dp</dimen> + + <!-- Dimensions for compact infobars are a little shorter. --> + <dimen name="infobar_compact_size">56dp</dimen> + <dimen name="infobar_compact_message_vertical_padding">8dp</dimen> + + <!-- Margin between items in the same control group. --> + <dimen name="infobar_control_margin_between_rows">8dp</dimen> + <dimen name="infobar_control_margin_between_columns">32dp</dimen> + <!-- Text size of the infobar message and other controls. --> + <dimen name="infobar_text_size">16sp</dimen> + <!-- Text size of the infobar message when a big icon is shown. --> + <dimen name="infobar_big_icon_message_size">20sp</dimen> + <!-- Margin between stacked buttons in an infobar. --> + <dimen name="infobar_margin_between_stacked_buttons">24dp</dimen> + <!-- Padding surrounding the infobar. --> + <dimen name="infobar_padding">16dp</dimen> + <!-- Minimum width of an infobar. --> + <dimen name="infobar_min_width">220dp</dimen> + + <!-- Dimensions applied to InfoBars with differently sized icons. --> + <dimen name="infobar_small_icon_size">24dp</dimen> + <dimen name="infobar_small_icon_margin">8dp</dimen> + <dimen name="infobar_big_icon_size">48dp</dimen> + <dimen name="infobar_big_icon_margin">16dp</dimen> + + <!-- Vertical margin applied between groups of controls. --> + <dimen name="infobar_margin_above_control_groups">24dp</dimen> + <dimen name="infobar_margin_above_button_row">32dp</dimen> +</resources> diff --git a/chromium/components/infobars/android/res/values/ids.xml b/chromium/components/infobars/android/res/values/ids.xml new file mode 100644 index 00000000000..e9fdf836382 --- /dev/null +++ b/chromium/components/infobars/android/res/values/ids.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. --> +<resources> + <!-- InfoBar constants --> + <item type="id" name="infobar_icon" /> + <item type="id" name="infobar_close_button" /> + <item type="id" name="infobar_message" /> +</resources> diff --git a/chromium/components/infobars/content/BUILD.gn b/chromium/components/infobars/content/BUILD.gn new file mode 100644 index 00000000000..6be0c41535c --- /dev/null +++ b/chromium/components/infobars/content/BUILD.gn @@ -0,0 +1,18 @@ +# 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. + +static_library("content") { + sources = [ + "content_infobar_manager.cc", + "content_infobar_manager.h", + ] + + public_deps = [ + "//base", + "//components/infobars/core", + "//content/public/browser", + "//content/public/common", + "//ui/base", + ] +} diff --git a/chromium/components/infobars/content/DEPS b/chromium/components/infobars/content/DEPS new file mode 100644 index 00000000000..c24130ef510 --- /dev/null +++ b/chromium/components/infobars/content/DEPS @@ -0,0 +1,4 @@ +include_rules = [ + "+content/public/browser", + "+content/public/common", +] diff --git a/chromium/components/infobars/content/content_infobar_manager.cc b/chromium/components/infobars/content/content_infobar_manager.cc new file mode 100644 index 00000000000..d93801b6c63 --- /dev/null +++ b/chromium/components/infobars/content/content_infobar_manager.cc @@ -0,0 +1,115 @@ +// Copyright (c) 2013 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. + +#include "components/infobars/content/content_infobar_manager.h" + +#include "base/command_line.h" +#include "components/infobars/core/confirm_infobar_delegate.h" +#include "components/infobars/core/infobar.h" +#include "content/public/browser/navigation_details.h" +#include "content/public/browser/navigation_entry.h" +#include "content/public/browser/navigation_handle.h" +#include "content/public/browser/web_contents.h" +#include "content/public/common/content_switches.h" +#include "ui/base/page_transition_types.h" + +namespace infobars { + +// static +InfoBarDelegate::NavigationDetails +ContentInfoBarManager::NavigationDetailsFromLoadCommittedDetails( + const content::LoadCommittedDetails& details) { + InfoBarDelegate::NavigationDetails navigation_details; + navigation_details.entry_id = details.entry->GetUniqueID(); + navigation_details.is_navigation_to_different_page = + details.is_navigation_to_different_page(); + navigation_details.did_replace_entry = details.did_replace_entry; + const ui::PageTransition transition = details.entry->GetTransitionType(); + navigation_details.is_reload = + ui::PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_RELOAD); + navigation_details.is_redirect = ui::PageTransitionIsRedirect(transition); + return navigation_details; +} + +// static +content::WebContents* ContentInfoBarManager::WebContentsFromInfoBar( + InfoBar* infobar) { + if (!infobar || !infobar->owner()) + return nullptr; + ContentInfoBarManager* infobar_manager = + static_cast<ContentInfoBarManager*>(infobar->owner()); + return infobar_manager->web_contents(); +} + +ContentInfoBarManager::ContentInfoBarManager(content::WebContents* web_contents) + : content::WebContentsObserver(web_contents), ignore_next_reload_(false) { + DCHECK(web_contents); + // Infobar animations cause viewport resizes. Disable them for automated + // tests, since they could lead to flakiness. + if (base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kEnableAutomation)) + set_animations_enabled(false); +} + +ContentInfoBarManager::~ContentInfoBarManager() { + ShutDown(); +} + +int ContentInfoBarManager::GetActiveEntryID() { + content::NavigationEntry* active_entry = + web_contents()->GetController().GetActiveEntry(); + return active_entry ? active_entry->GetUniqueID() : 0; +} + +std::unique_ptr<InfoBar> ContentInfoBarManager::CreateConfirmInfoBar( + std::unique_ptr<ConfirmInfoBarDelegate> delegate) { + NOTREACHED(); + return nullptr; +} + +void ContentInfoBarManager::RenderProcessGone(base::TerminationStatus status) { + RemoveAllInfoBars(true); +} + +void ContentInfoBarManager::DidStartNavigation( + content::NavigationHandle* navigation_handle) { + if (!navigation_handle->IsInMainFrame() || + navigation_handle->IsSameDocument()) { + return; + } + + ignore_next_reload_ = false; +} + +void ContentInfoBarManager::NavigationEntryCommitted( + const content::LoadCommittedDetails& load_details) { + const bool ignore = + ignore_next_reload_ && + ui::PageTransitionCoreTypeIs(load_details.entry->GetTransitionType(), + ui::PAGE_TRANSITION_RELOAD); + ignore_next_reload_ = false; + if (!ignore) + OnNavigation(NavigationDetailsFromLoadCommittedDetails(load_details)); +} + +void ContentInfoBarManager::WebContentsDestroyed() { + // Subclasses may override this method to destroy this object, so don't do + // anything here. +} + +void ContentInfoBarManager::OpenURL(const GURL& url, + WindowOpenDisposition disposition) { + // A normal user click on an infobar URL will result in a CURRENT_TAB + // disposition; turn that into a NEW_FOREGROUND_TAB so that we don't end up + // smashing the page the user is looking at. + web_contents()->OpenURL( + content::OpenURLParams(url, content::Referrer(), + (disposition == WindowOpenDisposition::CURRENT_TAB) + ? WindowOpenDisposition::NEW_FOREGROUND_TAB + : disposition, + ui::PAGE_TRANSITION_LINK, false)); + +} // namespace infobars + +} // namespace infobars diff --git a/chromium/components/infobars/content/content_infobar_manager.h b/chromium/components/infobars/content/content_infobar_manager.h new file mode 100644 index 00000000000..f9c123445ed --- /dev/null +++ b/chromium/components/infobars/content/content_infobar_manager.h @@ -0,0 +1,88 @@ +// 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. + +#ifndef COMPONENTS_INFOBARS_CONTENT_CONTENT_INFOBAR_MANAGER_H_ +#define COMPONENTS_INFOBARS_CONTENT_CONTENT_INFOBAR_MANAGER_H_ + +#include <memory> +#include <vector> + +#include "base/macros.h" +#include "build/build_config.h" +#include "components/infobars/core/infobar_manager.h" +#include "content/public/browser/reload_type.h" +#include "content/public/browser/web_contents_observer.h" +#include "content/public/browser/web_contents_user_data.h" +#include "ui/base/window_open_disposition.h" + +namespace content { +struct LoadCommittedDetails; +class WebContents; +} // namespace content + +namespace infobars { + +class InfoBar; + +// Associates a WebContents to an InfoBarManager. +// It manages the infobar notifications and responds to navigation events. +// By default the creation of confirm infobars is not supported. If embedders +// wish to add such support, they should create a custom subclass of +// ContentInfoBarManager that overrides CreateConfirmInfoBar(). +// This class is not itself a WebContentsUserData in order to support such +// subclassing; it is expected that embedders will either have an instance of +// this class as a member of their "Tab" objects or create a custom subclass +// that is a WCUD. +class ContentInfoBarManager : public InfoBarManager, + public content::WebContentsObserver { + public: + explicit ContentInfoBarManager(content::WebContents* web_contents); + ~ContentInfoBarManager() override; + + static InfoBarDelegate::NavigationDetails + NavigationDetailsFromLoadCommittedDetails( + const content::LoadCommittedDetails& details); + + // This function must only be called on infobars that are owned by a + // ContentInfoBarManager instance (or not owned at all, in which case this + // returns nullptr). + static content::WebContents* WebContentsFromInfoBar(InfoBar* infobar); + + // Makes it so the next reload is ignored. That is, if the next commit is a + // reload then it is treated as if nothing happened and no infobars are + // attempted to be closed. + // This is useful for non-user triggered reloads that should not dismiss + // infobars. For example, instant may trigger a reload when the google URL + // changes. + void set_ignore_next_reload() { ignore_next_reload_ = true; } + + // InfoBarManager: + // NOTE: By default this method is NOTREACHED() and returns nullptr. + // TODO(sdefresne): Change clients to invoke this on InfoBarManager + // and turn the method override private. + std::unique_ptr<InfoBar> CreateConfirmInfoBar( + std::unique_ptr<ConfirmInfoBarDelegate> delegate) override; + void OpenURL(const GURL& url, WindowOpenDisposition disposition) override; + + private: + // InfoBarManager: + int GetActiveEntryID() override; + + // content::WebContentsObserver: + void RenderProcessGone(base::TerminationStatus status) override; + void DidStartNavigation( + content::NavigationHandle* navigation_handle) override; + void NavigationEntryCommitted( + const content::LoadCommittedDetails& load_details) override; + void WebContentsDestroyed() override; + + // See description in set_ignore_next_reload(). + bool ignore_next_reload_; + + DISALLOW_COPY_AND_ASSIGN(ContentInfoBarManager); +}; + +} // namespace infobars + +#endif // COMPONENTS_INFOBARS_CONTENT_CONTENT_INFOBAR_MANAGER_H_ diff --git a/chromium/components/infobars/core/infobar_delegate.cc b/chromium/components/infobars/core/infobar_delegate.cc index 4f829ad8be8..2c5e77a7ac7 100644 --- a/chromium/components/infobars/core/infobar_delegate.cc +++ b/chromium/components/infobars/core/infobar_delegate.cc @@ -100,7 +100,8 @@ HungRendererInfoBarDelegate* InfoBarDelegate::AsHungRendererInfoBarDelegate() { return nullptr; } -PopupBlockedInfoBarDelegate* InfoBarDelegate::AsPopupBlockedInfoBarDelegate() { +blocked_content::PopupBlockedInfoBarDelegate* +InfoBarDelegate::AsPopupBlockedInfoBarDelegate() { return nullptr; } diff --git a/chromium/components/infobars/core/infobar_delegate.h b/chromium/components/infobars/core/infobar_delegate.h index cc6ac572da5..b1933ea71e7 100644 --- a/chromium/components/infobars/core/infobar_delegate.h +++ b/chromium/components/infobars/core/infobar_delegate.h @@ -13,9 +13,12 @@ class ConfirmInfoBarDelegate; class HungRendererInfoBarDelegate; -class PopupBlockedInfoBarDelegate; class ThemeInstalledInfoBarDelegate; +namespace blocked_content { +class PopupBlockedInfoBarDelegate; +} + #if defined(OS_ANDROID) namespace offline_pages { class OfflinePageInfoBarDelegate; @@ -164,6 +167,7 @@ class InfoBarDelegate { SYNC_ERROR_INFOBAR_DELEGATE_ANDROID = 97, MIXED_CONTENT_DOWNLOAD_INFOBAR_DELEGATE_ANDROID = 98, CONDITIONAL_TAB_STRIP_INFOBAR_ANDROID = 99, + LITE_MODE_HTTPS_IMAGE_COMPRESSION_INFOBAR_ANDROID = 100, }; // Describes navigation events, used to decide whether infobars should be @@ -256,7 +260,8 @@ class InfoBarDelegate { // Type-checking downcast routines: virtual ConfirmInfoBarDelegate* AsConfirmInfoBarDelegate(); virtual HungRendererInfoBarDelegate* AsHungRendererInfoBarDelegate(); - virtual PopupBlockedInfoBarDelegate* AsPopupBlockedInfoBarDelegate(); + virtual blocked_content::PopupBlockedInfoBarDelegate* + AsPopupBlockedInfoBarDelegate(); virtual ThemeInstalledInfoBarDelegate* AsThemePreviewInfobarDelegate(); virtual translate::TranslateInfoBarDelegate* AsTranslateInfoBarDelegate(); #if defined(OS_ANDROID) |