Commit f2ff0687 authored by Becky Zhou's avatar Becky Zhou Committed by Commit Bot

[TabModal] Add dialog view to tab modal javascript dialogs

Browser controls are accessible if the browser controls are not hidden
before the tab modal javascript dialog is shown. The app modal dialog
is also using the same dialog view now for consistency.

Bug: 687010
Change-Id: I6f36e8ef06129dd220d75d08878480672993fc72
Reviewed-on: https://chromium-review.googlesource.com/757166
Commit-Queue: Becky Zhou <huayinz@chromium.org>
Reviewed-by: default avatarTed Choc (back but slow, ping me) <tedchoc@chromium.org>
Reviewed-by: default avatarTheresa <twellington@chromium.org>
Cr-Commit-Position: refs/heads/master@{#526726}
parent f7106123
......@@ -54,6 +54,19 @@
android:inflatedId="@+id/bottombar"
android:layout="@layout/custom_tabs_bottombar" />
<ViewStub
android:id="@+id/tab_modal_dialog_container_stub"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inflatedId="@+id/tab_modal_dialog_container" />
<!-- Please do not add anything in between tab_modal_dialog_container_stub and
tab_modal_dialog_container_sibling_view. -->
<ViewStub
android:id="@+id/tab_modal_dialog_container_sibling_view"
android:layout_width="0dp"
android:layout_height="0dp" />
<org.chromium.chrome.browser.widget.FadingBackgroundView
android:id="@+id/fading_focus_target"
android:layout_width="match_parent"
......
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 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. -->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/scrim"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/modal_dialog_scrim_color" />
</FrameLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 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. -->
<!-- TODO(huayinz): rename menu_bg or change the dialog background to the desired one. -->
<org.chromium.chrome.browser.widget.BoundedLinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:chrome="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/menu_bg"
chrome:maxWidth="@dimen/dialog_max_width">
<android.support.v7.widget.DialogTitle
android:id="@+id/title"
android:textAppearance="@style/BlackHeadline2"
style="@style/AlertDialogContent" />
<ScrollView
android:layout_height="0dp"
android:layout_weight="1"
style="@style/AlertDialogContent">
<org.chromium.ui.widget.TextViewWithLeading
android:id="@+id/message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/BlackBody"
chrome:leading="20sp" />
</ScrollView>
<FrameLayout
android:id="@+id/custom"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<android.support.v7.widget.ButtonBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?attr/buttonBarStyle" >
<Button
android:id="@+id/negative_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?attr/buttonBarNegativeButtonStyle" />
<Button
android:id="@+id/positive_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?attr/buttonBarPositiveButtonStyle" />
</android.support.v7.widget.ButtonBarLayout>
</org.chromium.chrome.browser.widget.BoundedLinearLayout>
\ No newline at end of file
......@@ -19,7 +19,7 @@
android:layout_gravity="center"
android:orientation="vertical"
android:background="@drawable/menu_bg"
chrome:maxWidth="@dimen/promo_dialog_max_width" >
chrome:maxWidth="@dimen/dialog_max_width" >
<org.chromium.chrome.browser.widget.FadingEdgeScrollView
android:id="@+id/promo_container"
......
......@@ -124,6 +124,29 @@
<item name="chrometint">@color/dark_mode_tint</item>
</style>
<style name="ModalDialogTheme" parent="AlertDialogTheme">
<item name="android:windowFrame">@null</item>
<item name="android:backgroundDimEnabled">false</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowMinWidthMajor">100%</item>
<item name="android:windowMinWidthMinor">100%</item>
<item name="buttonBarStyle">@style/ModalDialogButtonBarStyle</item>
<item name="buttonBarButtonStyle">@style/ModalDialogButtonStyle</item>
</style>
<style name="ModalDialogButtonBarStyle" parent="Widget.AppCompat.ButtonBar.AlertDialog">
<item name="android:orientation">horizontal</item>
<item name="android:gravity">bottom|end</item>
<item name="android:paddingStart">@dimen/modal_dialog_control_padding_horizontal</item>
<item name="android:paddingEnd">@dimen/modal_dialog_control_padding_horizontal</item>
<item name="android:paddingTop">@dimen/modal_dialog_control_padding_vertical</item>
<item name="android:paddingBottom">@dimen/modal_dialog_control_padding_vertical</item>
</style>
<style name="ModalDialogButtonStyle" parent="Widget.AppCompat.Button.ButtonBar.AlertDialog">
<item name="android:textColor">@color/light_active_color</item>
</style>
<style name="SimpleDialog" parent="AlertDialogTheme">
<item name="windowNoTitle">true</item>
<item name="windowActionBar">false</item>
......
......@@ -139,7 +139,6 @@
<dimen name="promo_dialog_illustration_margin">24dp</dimen>
<dimen name="promo_dialog_illustration_width">150dp</dimen>
<dimen name="promo_dialog_padding">16dp</dimen>
<dimen name="promo_dialog_max_width">600dp</dimen>
<dimen name="promo_dialog_max_content_width">320dp</dimen>
<dimen name="promo_dialog_min_scrollable_height">100dp</dimen>
<dimen name="promo_dialog_title_text_size">23sp</dimen>
......@@ -190,6 +189,12 @@
<!-- Alert dialog -->
<dimen name="dialog_padding_top">@dimen/abc_dialog_padding_top_material</dimen>
<dimen name="dialog_padding_sides">@dimen/abc_dialog_padding_material</dimen>
<dimen name="dialog_max_width">600dp</dimen>
<!-- ModalDialogView dimensions -->
<dimen name="modal_dialog_control_padding_vertical">4dp</dimen>
<dimen name="modal_dialog_control_padding_horizontal">12dp</dimen>
<dimen name="tab_modal_scrim_vertical_margin">16dp</dimen>
<!-- Tab Strip Dimensions -->
<dimen name="tab_strip_height">0dp</dimen>
......
......@@ -93,6 +93,8 @@ import org.chromium.chrome.browser.metrics.LaunchMetrics;
import org.chromium.chrome.browser.metrics.StartupMetrics;
import org.chromium.chrome.browser.metrics.UmaSessionStats;
import org.chromium.chrome.browser.metrics.WebApkUma;
import org.chromium.chrome.browser.modaldialog.AppModalPresenter;
import org.chromium.chrome.browser.modaldialog.ModalDialogManager;
import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
import org.chromium.chrome.browser.net.spdyproxy.DataReductionProxySettings;
import org.chromium.chrome.browser.nfc.BeamController;
......@@ -246,6 +248,7 @@ public abstract class ChromeActivity extends AsyncInitializationActivity
private ContextualSearchManager mContextualSearchManager;
protected ReaderModeManager mReaderModeManager;
private SnackbarManager mSnackbarManager;
private ModalDialogManager mModalDialogManager;
private DataUseSnackbarController mDataUseSnackbarController;
private DataReductionPromoSnackbarController mDataReductionPromoSnackbarController;
private AppMenuPropertiesDelegate mAppMenuPropertiesDelegate;
......@@ -390,6 +393,8 @@ public abstract class ChromeActivity extends AsyncInitializationActivity
mBottomSheetContentController.init(mBottomSheet, mTabModelSelector, this);
}
((BottomContainer) findViewById(R.id.bottom_container)).initialize(mFullscreenManager);
mModalDialogManager = createModalDialogManager();
}
@Override
......@@ -1180,6 +1185,21 @@ public abstract class ChromeActivity extends AsyncInitializationActivity
return mSnackbarManager;
}
/**
* @return The {@link ModalDialogManager} created for this class.
*/
protected ModalDialogManager createModalDialogManager() {
return new ModalDialogManager(new AppModalPresenter(this), ModalDialogManager.APP_MODAL);
}
/**
* @return The {@link ModalDialogManager} that manages the display of modal dialogs (e.g.
* JavaScript dialogs).
*/
public ModalDialogManager getModalDialogManager() {
return mModalDialogManager;
}
protected Drawable getBackgroundDrawable() {
return new ColorDrawable(
ApiCompatibilityUtils.getColor(getResources(), R.color.light_background_color));
......
......@@ -85,6 +85,8 @@ import org.chromium.chrome.browser.metrics.LaunchMetrics;
import org.chromium.chrome.browser.metrics.MainIntentBehaviorMetrics;
import org.chromium.chrome.browser.metrics.StartupMetrics;
import org.chromium.chrome.browser.metrics.UmaUtils;
import org.chromium.chrome.browser.modaldialog.ModalDialogManager;
import org.chromium.chrome.browser.modaldialog.TabModalLifetimeHandler;
import org.chromium.chrome.browser.multiwindow.MultiInstanceChromeTabbedActivity;
import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
import org.chromium.chrome.browser.net.spdyproxy.DataReductionProxySettings;
......@@ -261,6 +263,8 @@ public class ChromeTabbedActivity
private ScreenshotMonitor mScreenshotMonitor;
private TabModalLifetimeHandler mTabModalHandler;
private boolean mUIInitialized;
private Boolean mMergeTabsOnResume;
......@@ -1901,6 +1905,8 @@ public class ChromeTabbedActivity
super.onOmniboxFocusChanged(hasFocus);
mMainIntentMetrics.onOmniboxFocused();
mTabModalHandler.onOmniboxFocusChanged(hasFocus);
}
private void recordBackPressedUma(String logMessage, @BackPressedResult int action) {
......@@ -1942,6 +1948,8 @@ public class ChromeTabbedActivity
if (getBottomSheet() != null && getBottomSheet().handleBackPress()) return true;
if (mTabModalHandler.handleBackPress()) return true;
if (currentTab == null) {
recordBackPressedUma("currentTab is null", BACK_PRESSED_TAB_IS_NULL);
moveTaskToBack(true);
......@@ -2158,6 +2166,11 @@ public class ChromeTabbedActivity
mUndoBarPopupController = null;
}
if (mTabModalHandler != null) {
mTabModalHandler.destroy();
mTabModalHandler = null;
}
super.onDestroyInternal();
FeatureUtilities.finalizePendingFeatures();
......@@ -2212,6 +2225,13 @@ public class ChromeTabbedActivity
return getLayoutManager().getOverviewListLayout();
}
@Override
protected ModalDialogManager createModalDialogManager() {
ModalDialogManager manager = super.createModalDialogManager();
mTabModalHandler = new TabModalLifetimeHandler(this, manager);
return manager;
}
// App Menu related code -----------------------------------------------------------------------
@Override
......
// Copyright 2017 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.jsdialog;
import android.support.annotation.StringRes;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.TextView;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.modaldialog.ModalDialogView;
/**
* The JavaScript dialog that is either app modal or tab modal.
*/
public class JavascriptModalDialogView extends ModalDialogView {
private final TextView mMessageView;
private final EditText mPromptEditText;
private final CheckBox mSuppressCheckBox;
private final View mScrollView;
/**
* Create a {@link JavascriptModalDialogView} with the specified properties.
* @param controller The controller for the dialog view.
* @param title The title of the dialog view.
* @param message The message of the dialog view.
* @param promptText The promptText of the dialog view. If null,
* prompt edit text will not be shown.
* @param shouldShowSuppressCheckBox Whether the suppress check box should be shown.
* @param positiveButtonTextId The string resource id of the positive button.
* @param negativeButtonTextId The string resource id of the negative button.
* @return A {@link JavascriptModalDialogView} with the specified properties.
*/
public static JavascriptModalDialogView create(Controller controller, String title,
String message, String promptText, boolean shouldShowSuppressCheckBox,
@StringRes int positiveButtonTextId, @StringRes int negativeButtonTextId) {
Params params = new Params();
params.title = title;
params.positiveButtonTextId = positiveButtonTextId;
params.negativeButtonTextId = negativeButtonTextId;
return new JavascriptModalDialogView(
controller, params, message, promptText, shouldShowSuppressCheckBox);
}
private JavascriptModalDialogView(Controller controller, Params params, String message,
String promptText, boolean shouldShowSuppressCheckBox) {
super(controller, params);
LayoutInflater inflater = LayoutInflater.from(getContext());
View customLayout = inflater.inflate(R.layout.js_modal_dialog, null);
params.customView = customLayout;
mScrollView = params.customView.findViewById(R.id.js_modal_dialog_scroll_view);
mMessageView = customLayout.findViewById(R.id.js_modal_dialog_message);
mPromptEditText = customLayout.findViewById(R.id.js_modal_dialog_prompt);
mSuppressCheckBox = customLayout.findViewById(R.id.suppress_js_modal_dialogs);
mMessageView.setText(message);
setPromptText(promptText);
setSuppressCheckBoxVisibility(shouldShowSuppressCheckBox);
}
@Override
protected void prepareBeforeShow() {
super.prepareBeforeShow();
// If the message is null or empty do not display the message text view.
// Hide parent scroll view instead of text view in order to prevent ui discrepancies.
if (mMessageView.getText().length() == 0) {
mScrollView.setVisibility(View.GONE);
} else {
// TODO(huayinz): See if View#canScrollVertictically() can be used for checking if
// scrollView is scrollable.
mScrollView.addOnLayoutChangeListener(
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
boolean isScrollable =
v.getMeasuredHeight() - v.getPaddingTop() - v.getPaddingBottom()
< ((ViewGroup) v).getChildAt(0).getMeasuredHeight();
v.setFocusable(isScrollable);
});
}
}
/**
* @param promptText Prompt text for prompt dialog. If null, prompt text is not visible.
*/
private void setPromptText(String promptText) {
if (promptText == null) return;
mPromptEditText.setVisibility(View.VISIBLE);
if (promptText.length() > 0) {
mPromptEditText.setText(promptText);
mPromptEditText.selectAll();
}
}
/**
* @return The prompt text edited by user.
*/
public String getPromptText() {
return mPromptEditText.getText().toString();
}
/**
* @param visible Whether the suppress check box should be visible. The check box should only
* be set visible if applicable for app modal JavaScript dialogs.
*/
private void setSuppressCheckBoxVisibility(boolean visible) {
mSuppressCheckBox.setVisibility(visible ? View.VISIBLE : View.GONE);
}
/**
* @return Whether the suppress check box is checked by user.
*/
public boolean isSuppressCheckBoxChecked() {
return mSuppressCheckBox.isChecked();
}
}
// Copyright 2017 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.modaldialog;
import android.app.Activity;
import android.app.Dialog;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.widget.AlwaysDismissedDialog;
/** The presenter that shows a {@link ModalDialogView} in an Android dialog. */
public class AppModalPresenter extends ModalDialogManager.Presenter {
private final Activity mActivity;
private Dialog mDialog;
public AppModalPresenter(Activity activity) {
mActivity = activity;
}
@Override
protected void addDialogView(View dialogView) {
mDialog = new AlwaysDismissedDialog(mActivity, R.style.ModalDialogTheme);
mDialog.setOnCancelListener(dialogInterface -> cancelCurrentDialog());
ViewGroup container = (ViewGroup) LayoutInflater.from(mActivity).inflate(
R.layout.modal_dialog_container, null);
mDialog.setContentView(container);
FrameLayout.LayoutParams params =
new FrameLayout.LayoutParams(ViewGroup.MarginLayoutParams.MATCH_PARENT,
ViewGroup.MarginLayoutParams.WRAP_CONTENT, Gravity.CENTER);
container.addView(dialogView, params);
mDialog.show();
}
@Override
protected void removeDialogView(View dialogView) {
// Dismiss the currently showing dialog.
if (mDialog != null) mDialog.dismiss();
mDialog = null;
}
}
// Copyright 2017 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.modaldialog;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Pair;
import android.util.SparseArray;
import android.view.View;
import org.chromium.base.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
/**
* Manager for managing the display of a queue of {@link ModalDialogView}s.
*/
public class ModalDialogManager {
/**
* Present a {@link ModalDialogView} in a container.
*/
public static abstract class Presenter {
private Runnable mCancelCallback;
private ModalDialogView mModalDialog;
private View mCurrentView;
/**
* @param dialog The dialog that's currently showing in this presenter. If null, no dialog
* is currently showing.
*/
private void setModalDialog(
@Nullable ModalDialogView dialog, @Nullable Runnable cancelCallback) {
if (dialog == null) {
removeDialogView(mCurrentView);
mModalDialog = null;
mCancelCallback = null;
} else {
assert mModalDialog
== null : "Should call setModalDialog(null) before setting a modal dialog.";
mModalDialog = dialog;
mCurrentView = dialog.getView();
mCancelCallback = cancelCallback;
addDialogView(mCurrentView);
}
}
/**
* Run the cached cancel callback and reset the cached callback.
*/
protected final void cancelCurrentDialog() {
if (mCancelCallback == null) return;
// Set #mCancelCallback to null before calling the callback to avoid it being
// updated during the callback.
Runnable callback = mCancelCallback;
mCancelCallback = null;
callback.run();
}
/**
* @return The modal dialog that this presenter is showing.
*/
protected final ModalDialogView getModalDialog() {
return mModalDialog;
}
/**
* Add the specified {@link ModalDialogView} in a container.
* @param dialogView The {@link ModalDialogView} that needs to be shown.
*/
protected abstract void addDialogView(View dialogView);
/**
* Remove the specified {@link ModalDialogView} from a container.
* @param dialogView The {@link ModalDialogView} that needs to be removed.
*/
protected abstract void removeDialogView(View dialogView);
}
@Retention(RetentionPolicy.SOURCE)
@IntDef({APP_MODAL, TAB_MODAL})
public @interface ModalDialogType {}
public static final int APP_MODAL = 0;
public static final int TAB_MODAL = 1;
/** Mapping of the {@link Presenter}s and the type of dialogs they are showing. */
private final SparseArray<Presenter> mPresenters = new SparseArray<>();
/** The list of pending dialogs */
private final List<Pair<ModalDialogView, Integer>> mPendingDialogs = new ArrayList<>();
/** The default presenter to be used if a specified type is not supported. */
private final Presenter mDefaultPresenter;
/** The presenter of the type of the dialog that is currently showing. */
private Presenter mCurrentPresenter;
/**
* Constructor for initializing default {@link Presenter}.
* @param defaultPresenter The default presenter to be used when no presenter specified.
* @param defaultType The dialog type of the default presenter.
*/
public ModalDialogManager(
@NonNull Presenter defaultPresenter, @ModalDialogType int defaultType) {
mDefaultPresenter = defaultPresenter;
registerPresenter(defaultPresenter, defaultType);
}
/**
* Register a {@link Presenter} that shows a specific type of dialog. Note that only one
* presenter of each type can be registered.
* @param presenter The {@link Presenter} to be registered.
* @param dialogType The type of the dialog shown by the specified presenter.
*/
public void registerPresenter(Presenter presenter, @ModalDialogType int dialogType) {
assert mPresenters.get(dialogType)
== null : "Only one presenter can be registered for each type.";
mPresenters.put(dialogType, presenter);
}
/**
* @return Whether a dialog is currently showing.
*/
public boolean isShowing() {
return mCurrentPresenter != null;
}
/**
* Show the specified dialog. If another dialog is currently showing, the specified dialog will
* be added to the pending dialog list.
* @param dialog The dialog to be shown or added to pending list.
* @param dialogType The type of the dialog to be shown.
*/
public void showDialog(ModalDialogView dialog, @ModalDialogType int dialogType) {
if (isShowing()) {
mPendingDialogs.add(Pair.create(dialog, dialogType));
return;
}
dialog.prepareBeforeShow();
mCurrentPresenter = mPresenters.get(dialogType, mDefaultPresenter);
mCurrentPresenter.setModalDialog(dialog, () -> cancelDialog(dialog));
}
/**
* Dismiss the specified dialog. If the dialog is not currently showing, it will be removed from
* the pending dialog list.
* @param dialog The dialog to be dismissed or removed from pending list.
*/
public void dismissDialog(ModalDialogView dialog) {
if (dialog != mCurrentPresenter.getModalDialog()) {
for (int i = 0; i < mPendingDialogs.size(); ++i) {
if (mPendingDialogs.get(i).first == dialog) {
mPendingDialogs.remove(i);
break;
}
}
return;
}
if (!isShowing()) return;
assert dialog == mCurrentPresenter.getModalDialog();
mCurrentPresenter.setModalDialog(null, null);
mCurrentPresenter = null;
if (!mPendingDialogs.isEmpty()) {
Pair<ModalDialogView, Integer> nextDialog = mPendingDialogs.remove(0);
showDialog(nextDialog.first, nextDialog.second);
}
}
/**
* Cancel showing the specified dialog. This is essentially the same as
* {@link #dismissDialog(ModalDialogView)} but will also call the onCancelled callback from the
* modal dialog.
* @param dialog The dialog to be cancelled.
*/
public void cancelDialog(ModalDialogView dialog) {
dismissDialog(dialog);
dialog.getController().onCancel();
}
/**
* Dismiss the dialog currently shown and remove all pending dialogs and call the onCancelled
* callbacks from the modal dialogs.
*/
protected void cancelAllDialogs() {
while (!mPendingDialogs.isEmpty()) {
mPendingDialogs.remove(0).first.getController().onCancel();
}
if (isShowing()) cancelDialog(mCurrentPresenter.getModalDialog());
}
@VisibleForTesting
List getPendingDialogsForTest() {
return mPendingDialogs;
}
@VisibleForTesting
Presenter getPresenterForTest(@ModalDialogType int dialogType) {
return mPresenters.get(dialogType);
}
@VisibleForTesting
Presenter getCurrentPresenterForTest() {
return mCurrentPresenter;
}
}
// Copyright 2017 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.modaldialog;
import android.content.Context;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
import android.text.TextUtils;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import org.chromium.base.ContextUtils;
import org.chromium.chrome.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Generic builder for app modal or tab modal alert dialogs.
*/
public class ModalDialogView implements View.OnClickListener {
/**
* Interface that controls the actions on the modal dialog.
*/
public interface Controller {
/**
* Handle click event of the buttons on the dialog.
* @param buttonType The type of the button.
*/
void onClick(@ButtonType int buttonType);
/**
* Handle dismiss event when the dialog is not dismissed by actions on the dialog such as
* back press, and on tab modal dialog, tab switcher button click.
*/
void onCancel();
}
/** Parameters that can be used to create a new ModalDialogView. */
public static class Params {
/** Optional: The String to show as the dialog title. */
public String title;
/** Optional: The String to show as descriptive text. */
public String message;
/**
* Optional: The customized View to show in the dialog. Note that the message and the
* custom view cannot be set together.
*/
public View customView;
/** Optional: Resource ID of the String to show on the positive button. */
public @StringRes int positiveButtonTextId;
/** Optional: Resource ID of the String to show on the negative button. */
public @StringRes int negativeButtonTextId;
}
@IntDef({BUTTON_POSITIVE, BUTTON_NEGATIVE})
@Retention(RetentionPolicy.SOURCE)
public @interface ButtonType {}
public static final int BUTTON_POSITIVE = 0;
public static final int BUTTON_NEGATIVE = 1;
private final Controller mController;
private final Context mContext;
private final Params mParams;
private final View mDialogView;
private final TextView mTitleView;
private final TextView mMessageView;
private final ViewGroup mCustomView;
private final Button mPositiveButton;
private final Button mNegativeButton;
/**
* Constructor for initializing controller and views.
* @param controller The controller for this dialog.
*/
public ModalDialogView(@NonNull Controller controller, @NonNull Params params) {
mController = controller;
mContext = new ContextThemeWrapper(
ContextUtils.getApplicationContext(), R.style.ModalDialogTheme);
mParams = params;
mDialogView = LayoutInflater.from(mContext).inflate(R.layout.modal_dialog_view, null);
mTitleView = mDialogView.findViewById(R.id.title);
mMessageView = mDialogView.findViewById(R.id.message);
mCustomView = mDialogView.findViewById(R.id.custom);
mPositiveButton = mDialogView.findViewById(R.id.positive_button);
mNegativeButton = mDialogView.findViewById(R.id.negative_button);
}
@Override
public void onClick(View view) {
if (view == mPositiveButton) {
mController.onClick(BUTTON_POSITIVE);
} else if (view == mNegativeButton) {
mController.onClick(BUTTON_NEGATIVE);
}
}
/**
* Prepare the contents before showing the dialog.
*/
protected void prepareBeforeShow() {
if (TextUtils.isEmpty(mParams.title)) {
mTitleView.setVisibility(View.GONE);
} else {
mTitleView.setText(mParams.title);
}
if (TextUtils.isEmpty(mParams.message)) {
((View) mMessageView.getParent()).setVisibility(View.GONE);
} else {
assert mParams.customView == null;
mMessageView.setText(mParams.message);
}
if (mParams.customView != null) {
if (mParams.customView.getParent() != null) {
((ViewGroup) mParams.customView.getParent()).removeView(mParams.customView);
}
mCustomView.addView(mParams.customView);
} else {
mCustomView.setVisibility(View.GONE);
}
if (mParams.positiveButtonTextId == 0) {
mPositiveButton.setVisibility(View.GONE);
} else {
mPositiveButton.setText(mParams.positiveButtonTextId);
mPositiveButton.setOnClickListener(this);
}
if (mParams.negativeButtonTextId == 0) {
mNegativeButton.setVisibility(View.GONE);
} else {
mNegativeButton.setText(mParams.negativeButtonTextId);
mNegativeButton.setOnClickListener(this);
}
}
/**
* @return The {@link Context} with the modal dialog theme set.
*/
public Context getContext() {
return mContext;
}
/**
* @return The content view of this dialog.
*/
public View getView() {
return mDialogView;
}
/**
* @return The controller that controls the actions on the dialogs.
*/
public Controller getController() {
return mController;
}
}
// Copyright 2017 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.modaldialog;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabObserver;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabModelObserver;
/**
* Class responsible for handling dismissal of a tab modal dialog on user actions outside the tab
* modal dialog.
*/
public class TabModalLifetimeHandler {
/** The observer to dismiss all dialogs when the attached tab is not interactable. */
private final TabObserver mTabObserver = new EmptyTabObserver() {
@Override
public void onInteractabilityChanged(boolean isInteractable) {
if (!isInteractable && mPresenter.getModalDialog() != null) {
mManager.cancelAllDialogs();
}
}
};
private final ModalDialogManager mManager;
private final TabModalPresenter mPresenter;
private final TabModelSelectorTabModelObserver mTabModelObserver;
private final boolean mHasBottomControls;
private Tab mActiveTab;
/**
* @param activity The {@link ChromeActivity} that this handler is attached to.
* @param manager The {@link ModalDialogManager} that this handler handles.
*/
public TabModalLifetimeHandler(ChromeActivity activity, ModalDialogManager manager) {
mManager = manager;
mPresenter = new TabModalPresenter(activity);
mManager.registerPresenter(mPresenter, ModalDialogManager.TAB_MODAL);
mHasBottomControls = activity.getBottomSheet() != null;
TabModelSelector tabModelSelector = activity.getTabModelSelector();
mTabModelObserver = new TabModelSelectorTabModelObserver(tabModelSelector) {
@Override
public void didSelectTab(Tab tab, TabModel.TabSelectionType type, int lastId) {
if (mActiveTab != null) mActiveTab.removeObserver(mTabObserver);
mActiveTab = tabModelSelector.getCurrentTab();
if (mActiveTab != null) mActiveTab.addObserver(mTabObserver);
}
};
}
/**
* Notified when the focus of the omnibox has changed.
* @param hasFocus Whether the omnibox currently has focus.
*/
public void onOmniboxFocusChanged(boolean hasFocus) {
// If has bottom controls, the view hierarchy will be updated by mBottomSheetObserver.
if (mPresenter.getModalDialog() != null && !mHasBottomControls) {
mPresenter.updateContainerHierarchy(!hasFocus);
}
}
/**
* Handle a back press event.
*/
public boolean handleBackPress() {
if (mPresenter.getModalDialog() == null) return false;
mPresenter.cancelCurrentDialog();
return true;
}
/**
* Remove any remaining dependencies.
*/
public void destroy() {
mTabModelObserver.destroy();
}
}
// Copyright 2017 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.modaldialog;
import android.content.res.Resources;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.ViewStub;
import android.widget.FrameLayout;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel;
import org.chromium.chrome.browser.contextualsearch.ContextualSearchManager;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheet;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetObserver;
import org.chromium.chrome.browser.widget.bottomsheet.EmptyBottomSheetObserver;
import org.chromium.content.browser.ContentViewCore;
/**
* The presenter that displays a single tab modal dialog.
*/
public class TabModalPresenter extends ModalDialogManager.Presenter {
/** The activity displaying the dialogs. */
private final ChromeActivity mChromeActivity;
/** Whether browser controls are at the bottom */
private final boolean mHasBottomControls;
/** The active tab of which the dialog will be shown on top. */
private Tab mActiveTab;
/**
* The observer to change view hierarchy for the dialog container when the sheet is opened or
* closed.
*/
private BottomSheetObserver mBottomSheetObserver;
/** The parent view that contains the dialog container. */
private ViewGroup mContainerParent;
/** The container view that a dialog to be shown will be attached to. */
private ViewGroup mDialogContainer;
/** Whether the dialog container is brought to the front in its parent. */
private boolean mContainerIsAtFront;
/** Whether the action bar on selected text is temporarily cleared for showing dialogs. */
private boolean mDidClearTextControls;
/**
* The sibling view of the dialog container drawn next in its parent when it should be behind
* browser controls. If BottomSheet is opened or UrlBar is focused, the dialog container should
* be behind the browser controls and the URL suggestions.
*/
private View mDefaultNextSiblingView;
/**
* Constructor for initializing dialog container.
* @param chromeActivity The activity displaying the dialogs.
*/
public TabModalPresenter(ChromeActivity chromeActivity) {
mChromeActivity = chromeActivity;
mHasBottomControls = mChromeActivity.getBottomSheet() != null;
if (mHasBottomControls) {
mBottomSheetObserver = new EmptyBottomSheetObserver() {
@Override
public void onSheetOpened(@BottomSheet.StateChangeReason int reason) {
updateContainerHierarchy(false);
}
@Override
public void onSheetClosed(@BottomSheet.StateChangeReason int reason) {
updateContainerHierarchy(true);
}
};
}
}
@Override
protected void addDialogView(View dialogView) {
if (mDialogContainer == null) initDialogContainer();
setBrowserControlsAccess(true);
mDialogContainer.setVisibility(View.VISIBLE);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
MarginLayoutParams.MATCH_PARENT, MarginLayoutParams.WRAP_CONTENT, Gravity.CENTER);
mDialogContainer.addView(dialogView, params);
mChromeActivity.addViewObscuringAllTabs(mDialogContainer);
}
@Override
protected void removeDialogView(View dialogView) {
setBrowserControlsAccess(false);
mDialogContainer.removeView(dialogView);
mDialogContainer.setVisibility(View.GONE);
mChromeActivity.removeViewObscuringAllTabs(mDialogContainer);
}
/**
* Change view hierarchy for the dialog container to be either the front most or beneath the
* toolbar.
* @param toFront Whether the dialog container should be brought to the front.
*/
void updateContainerHierarchy(boolean toFront) {
if (toFront == mContainerIsAtFront) return;
mContainerIsAtFront = toFront;
if (toFront) {
mDialogContainer.bringToFront();
} else {
mContainerParent.removeView(mDialogContainer);
mContainerParent.addView(
mDialogContainer, mContainerParent.indexOfChild(mDefaultNextSiblingView));
}
}
/**
* Inflate the dialog container in the dialog container view stub.
*/
private void initDialogContainer() {
ViewStub dialogContainerStub =
mChromeActivity.findViewById(R.id.tab_modal_dialog_container_stub);
dialogContainerStub.setLayoutResource(R.layout.modal_dialog_container);
mDialogContainer = (ViewGroup) dialogContainerStub.inflate();
mContainerParent = (ViewGroup) mDialogContainer.getParent();
// The default sibling view is the next view of the dialog container stub in main.xml and
// should not be removed from its parent.
mDefaultNextSiblingView =
mChromeActivity.findViewById(R.id.tab_modal_dialog_container_sibling_view);
assert mDefaultNextSiblingView != null;
// Set the margin of the container and the dialog scrim so that the scrim doesn't overlap
// the toolbar.
Resources resources = mChromeActivity.getResources();
int scrimVerticalMargin =
resources.getDimensionPixelSize(R.dimen.tab_modal_scrim_vertical_margin);
int containerVerticalMargin =
resources.getDimensionPixelSize(mChromeActivity.getControlContainerHeightResource())
- scrimVerticalMargin;
MarginLayoutParams params = (MarginLayoutParams) mDialogContainer.getLayoutParams();
params.width = ViewGroup.MarginLayoutParams.MATCH_PARENT;
params.height = ViewGroup.MarginLayoutParams.MATCH_PARENT;
params.topMargin = !mHasBottomControls ? containerVerticalMargin : 0;
params.bottomMargin = mHasBottomControls ? containerVerticalMargin : 0;
mDialogContainer.setLayoutParams(params);
View scrimView = mDialogContainer.findViewById(R.id.scrim);
params = (MarginLayoutParams) scrimView.getLayoutParams();
params.width = MarginLayoutParams.MATCH_PARENT;
params.height = MarginLayoutParams.MATCH_PARENT;
params.topMargin = !mHasBottomControls ? scrimVerticalMargin : 0;
params.bottomMargin = mHasBottomControls ? scrimVerticalMargin : 0;
scrimView.setLayoutParams(params);
}
/**
* Set whether the browser controls access should be restricted. If true, dialogs are expected
* to be showing and overflow menu would be disabled.
* @param restricted Whether the browser controls access should be restricted.
*/
private void setBrowserControlsAccess(boolean restricted) {
BottomSheet bottomSheet = mChromeActivity.getBottomSheet();
View menuButton = mChromeActivity.getToolbarManager().getMenuButton();
if (restricted) {
mActiveTab = mChromeActivity.getActivityTab();
assert mActiveTab
!= null : "Tab modal dialogs should be shown on top of an active tab.";
// Hide contextual search panel so that bottom toolbar will not be
// obscured and back press is not overridden.
ContextualSearchManager contextualSearchManager =
mChromeActivity.getContextualSearchManager();
if (contextualSearchManager != null) {
contextualSearchManager.hideContextualSearch(
OverlayPanel.StateChangeReason.UNKNOWN);
}
// Dismiss the action bar that obscures the dialogs but preserve the text selection.
ContentViewCore contentViewCore = mActiveTab.getContentViewCore();
if (contentViewCore != null) {
contentViewCore.preserveSelectionOnNextLossOfFocus();
contentViewCore.getContainerView().clearFocus();
contentViewCore.updateTextSelectionUI(false);
mDidClearTextControls = true;
}
// Force toolbar to show and disable overflow menu.
// TODO(huayinz): figure out a way to avoid |UpdateBrowserControlsState| being blocked
// by render process stalled due to javascript dialog.
mActiveTab.onTabModalDialogStateChanged(true);
if (mHasBottomControls) {
bottomSheet.setSheetState(BottomSheet.SHEET_STATE_PEEK, true);
bottomSheet.addObserver(mBottomSheetObserver);
} else {
mChromeActivity.getToolbarManager().setUrlBarFocus(false);
}
menuButton.setEnabled(false);
updateContainerHierarchy(true);
} else {
// Show the action bar back if it was dismissed when the dialogs were showing.
ContentViewCore contentViewCore = mActiveTab.getContentViewCore();
if (mDidClearTextControls) {
mDidClearTextControls = false;
if (contentViewCore != null) {
contentViewCore.updateTextSelectionUI(true);
}
}
mActiveTab.onTabModalDialogStateChanged(false);
menuButton.setEnabled(true);
if (mHasBottomControls) bottomSheet.removeObserver(mBottomSheetObserver);
mActiveTab = null;
}
}
@VisibleForTesting
View getDialogContainerForTest() {
return mDialogContainer;
}
@VisibleForTesting
ViewGroup getContainerParentForTest() {
return mContainerParent;
}
}
......@@ -234,6 +234,7 @@ public class Tab
private boolean mIsClosing;
private boolean mIsShowingErrorPage;
private boolean mIsShowingTabModalDialog;
private Bitmap mFavicon;
......@@ -805,6 +806,13 @@ public class Tab
return getWebContents() != null && getWebContents().isShowingInterstitialPage();
}
/**
* @return Whether a tab modal dialog is showing.
*/
public boolean isShowingTabModalDialog() {
return mIsShowingTabModalDialog;
}
/**
* @return Whether the {@link Tab} is currently showing an error page.
*/
......@@ -3358,6 +3366,17 @@ public class Tab
hideMediaDownloadInProductHelp();
}
/**
* Handle browser controls when a tab modal dialog is shown.
* @param isShowing Whether a tab modal dialog is showing.
*/
public void onTabModalDialogStateChanged(boolean isShowing) {
mIsShowingTabModalDialog = isShowing;
if (mFullscreenManager == null) return;
mFullscreenManager.setPositionsForTabToNonFullscreen();
updateBrowserControlsState(BrowserControlsState.SHOWN, false);
}
@CalledByNative
private void showMediaDownloadInProductHelp(int x, int y, int width, int height) {
// If we are not currently showing the widget, ask the tracker if we can show it.
......
......@@ -153,6 +153,7 @@ public class TabStateBrowserControlsVisibilityDelegate
enableHidingBrowserControls &= (mTab.getFullscreenManager() != null);
enableHidingBrowserControls &= DeviceClassManager.enableFullscreen();
enableHidingBrowserControls &= !mIsFullscreenWaitingForLoad;
enableHidingBrowserControls &= !mTab.isShowingTabModalDialog();
return enableHidingBrowserControls;
}
......
......@@ -559,6 +559,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/invalidation/InvalidationController.java",
"java/src/org/chromium/chrome/browser/invalidation/InvalidationServiceFactory.java",
"java/src/org/chromium/chrome/browser/invalidation/UniqueIdInvalidationClientNameGenerator.java",
"java/src/org/chromium/chrome/browser/jsdialog/JavascriptModalDialogView.java",
"java/src/org/chromium/chrome/browser/locale/DefaultSearchEngineDialogHelper.java",
"java/src/org/chromium/chrome/browser/locale/DefaultSearchEnginePromoDialog.java",
"java/src/org/chromium/chrome/browser/locale/LocaleManager.java",
......@@ -632,6 +633,11 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/metrics/VariationsSession.java",
"java/src/org/chromium/chrome/browser/metrics/WebApkUma.java",
"java/src/org/chromium/chrome/browser/metrics/WebappUma.java",
"java/src/org/chromium/chrome/browser/modaldialog/AppModalPresenter.java",
"java/src/org/chromium/chrome/browser/modaldialog/ModalDialogManager.java",
"java/src/org/chromium/chrome/browser/modaldialog/ModalDialogView.java",
"java/src/org/chromium/chrome/browser/modaldialog/TabModalLifetimeHandler.java",
"java/src/org/chromium/chrome/browser/modaldialog/TabModalPresenter.java",
"java/src/org/chromium/chrome/browser/mojo/ChromeInterfaceRegistrar.java",
"java/src/org/chromium/chrome/browser/multiwindow/MultiInstanceChromeTabbedActivity.java",
"java/src/org/chromium/chrome/browser/multiwindow/MultiWindowUtils.java",
......@@ -1601,6 +1607,7 @@ chrome_test_java_sources = [
"javatests/src/org/chromium/chrome/browser/metrics/PageLoadMetricsTest.java",
"javatests/src/org/chromium/chrome/browser/metrics/StartupLoadingMetricsTest.java",
"javatests/src/org/chromium/chrome/browser/metrics/UkmIncognitoTest.java",
"javatests/src/org/chromium/chrome/browser/modaldialog/ModalDialogManagerTest.java",
"javatests/src/org/chromium/chrome/browser/multiwindow/MultiWindowIntegrationTest.java",
"javatests/src/org/chromium/chrome/browser/multiwindow/MultiWindowTestHelper.java",
"javatests/src/org/chromium/chrome/browser/multiwindow/MultiWindowUtilsTest.java",
......
// Copyright 2017 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.modaldialog;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.isEnabled;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.not;
import android.support.test.espresso.Espresso;
import android.support.test.filters.SmallTest;
import android.view.View;
import android.view.ViewGroup;
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.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
/**
* Tests for displaying and functioning of modal dialogs on tabs.
*/
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class ModalDialogManagerTest {
@Rule
public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();
private static final int MAX_DIALOGS = 3;
private ChromeTabbedActivity mActivity;
private ModalDialogManager mManager;
private ModalDialogView[] mModalDialogViews;
@Before
public void setUp() throws Exception {
mActivityTestRule.startMainActivityOnBlankPage();
mActivity = mActivityTestRule.getActivity();
mManager = mActivity.getModalDialogManager();
mModalDialogViews = new ModalDialogView[MAX_DIALOGS];
for (int i = 0; i < MAX_DIALOGS; i++) mModalDialogViews[i] = createDialog(i);
}
@Test
@SmallTest
public void testOneDialog() throws Exception {
// Initially there are no dialogs in the pending list. Browser controls are not restricted.
checkPendingSize(0);
checkBrowserControls(false);
checkCurrentPresenter(null);
// Show a dialog. The pending list should be empty, and the dialog should be showing.
// Browser controls should be restricted.
showDialog(0, ModalDialogManager.TAB_MODAL);
checkPendingSize(0);
onView(withId(R.id.tab_modal_dialog_container))
.check(matches(hasDescendant(withText("0"))));
checkBrowserControls(true);
checkCurrentPresenter(ModalDialogManager.TAB_MODAL);
// Dismiss the dialog by clicking positive button.
onView(withText(R.string.ok)).perform(click());
checkPendingSize(0);
onView(withId(R.id.tab_modal_dialog_container))
.check(matches(not(hasDescendant(withText("0")))));
checkBrowserControls(false);
checkCurrentPresenter(null);
}
@Test
@SmallTest
public void testTwoDialogs() throws Exception {
// Initially there are no dialogs in the pending list. Browser controls are not restricted.
checkPendingSize(0);
checkBrowserControls(false);
checkCurrentPresenter(null);
// Show the first dialog.
// The pending list should be empty, and the dialog should be showing.
// The tab modal container shouldn't be in the window hierarchy when an app modal dialog is
// showing.
showDialog(0, ModalDialogManager.APP_MODAL);
checkPendingSize(0);
onView(withText("0")).check(matches(isDisplayed()));
onView(withId(R.id.tab_modal_dialog_container)).check(doesNotExist());
checkCurrentPresenter(ModalDialogManager.APP_MODAL);
// Show the second dialog. It should be added to the pending list, and the first dialog
// should still be shown.
showDialog(1, ModalDialogManager.TAB_MODAL);
checkPendingSize(1);
onView(withText("0")).check(matches(isDisplayed()));
onView(withId(R.id.tab_modal_dialog_container)).check(doesNotExist());
checkCurrentPresenter(ModalDialogManager.APP_MODAL);
// Dismiss the first dialog by clicking cancel. The second dialog should be removed from
// pending list and shown immediately after.
onView(withText(R.string.cancel)).perform(click());
checkPendingSize(0);
onView(withText("0")).check(doesNotExist());
onView(withId(R.id.tab_modal_dialog_container))
.check(matches(
allOf(not(hasDescendant(withText("0"))), hasDescendant(withText("1")))));
checkBrowserControls(true);
checkCurrentPresenter(ModalDialogManager.TAB_MODAL);
// Dismiss the second dialog by clicking ok. Browser controls should no longer be
// restricted.
onView(withText(R.string.ok)).perform(click());
checkPendingSize(0);
onView(withText("0")).check(doesNotExist());
onView(withId(R.id.tab_modal_dialog_container))
.check(matches(allOf(
not(hasDescendant(withText("0"))), not(hasDescendant(withText("1"))))));
checkBrowserControls(false);
checkCurrentPresenter(null);
}
@Test
@SmallTest
public void testThreeDialogs() throws Exception {
// Initially there are no dialogs in the pending list. Browser controls are not restricted.
checkPendingSize(0);
checkBrowserControls(false);
checkCurrentPresenter(null);
// Show the first dialog.
// The pending list should be empty, and the dialog should be showing.
// Browser controls should be restricted.
showDialog(0, ModalDialogManager.TAB_MODAL);
checkPendingSize(0);
onView(withId(R.id.tab_modal_dialog_container))
.check(matches(hasDescendant(withText("0"))));
checkBrowserControls(true);
checkCurrentPresenter(ModalDialogManager.TAB_MODAL);
// Show the second dialog. It should be added to the pending list, and the first dialog
// should still be shown.
showDialog(1, ModalDialogManager.TAB_MODAL);
checkPendingSize(1);
onView(withId(R.id.tab_modal_dialog_container))
.check(matches(
allOf(hasDescendant(withText("0")), not(hasDescendant(withText("1"))))));
checkBrowserControls(true);
checkCurrentPresenter(ModalDialogManager.TAB_MODAL);
// Show the third dialog. It should be added to the pending list, and the first dialog
// should still be shown.
showDialog(2, ModalDialogManager.APP_MODAL);
checkPendingSize(2);
onView(withText("2")).check(doesNotExist());
onView(withId(R.id.tab_modal_dialog_container))
.check(matches(allOf(hasDescendant(withText("0")),
not(hasDescendant(withText("1"))), not(hasDescendant(withText("2"))))));
checkBrowserControls(true);
checkCurrentPresenter(ModalDialogManager.TAB_MODAL);
// Stimulate dismissing the dialog by non-user action. The second dialog should be removed
// from pending list without showing.
dismissDialog(1);
checkPendingSize(1);
onView(withText("2")).check(doesNotExist());
onView(withId(R.id.tab_modal_dialog_container))
.check(matches(allOf(hasDescendant(withText("0")),
not(hasDescendant(withText("1"))), not(hasDescendant(withText("2"))))));
checkBrowserControls(true);
checkCurrentPresenter(ModalDialogManager.TAB_MODAL);
// Dismiss the second dialog twice and verify nothing breaks.
dismissDialog(1);
checkPendingSize(1);
onView(withText("2")).check(doesNotExist());
onView(withId(R.id.tab_modal_dialog_container))
.check(matches(allOf(hasDescendant(withText("0")),
not(hasDescendant(withText("1"))), not(hasDescendant(withText("2"))))));
checkCurrentPresenter(ModalDialogManager.TAB_MODAL);
// Dismiss the first dialog. The third dialog should be removed from pending list and
// shown immediately after. The tab modal container shouldn't be in the window hierarchy
// when an app modal dialog is showing.
dismissDialog(0);
checkPendingSize(0);
onView(withText("2")).check(matches(isDisplayed()));
onView(withId(R.id.tab_modal_dialog_container)).check(doesNotExist());
checkCurrentPresenter(ModalDialogManager.APP_MODAL);
// Dismiss the third dialog by clicking OK. Browser controls should no longer be restricted.
onView(withText(R.string.ok)).perform(click());
checkPendingSize(0);
onView(withText("2")).check(doesNotExist());
onView(withId(R.id.tab_modal_dialog_container))
.check(matches(allOf(not(hasDescendant(withText("0"))),
not(hasDescendant(withText("1"))), not(hasDescendant(withText("2"))))));
checkBrowserControls(false);
checkCurrentPresenter(null);
}
@Test
@SmallTest
public void testShow_UrlBarFocused() throws Exception {
// Show a dialog. The dialog should be shown on top of the toolbar.
showDialog(0, ModalDialogManager.TAB_MODAL);
TabModalPresenter presenter =
(TabModalPresenter) mManager.getPresenterForTest(ModalDialogManager.TAB_MODAL);
final View dialogContainer = presenter.getDialogContainerForTest();
final View controlContainer = mActivity.findViewById(R.id.control_container);
final ViewGroup containerParent = presenter.getContainerParentForTest();
ThreadUtils.runOnUiThreadBlocking(() -> {
Assert.assertTrue(containerParent.indexOfChild(dialogContainer)
> containerParent.indexOfChild(controlContainer));
});
// When editing URL, it should be shown on top of the dialog.
onView(withId(R.id.url_bar)).perform(click());
ThreadUtils.runOnUiThreadBlocking(() -> {
Assert.assertTrue(containerParent.indexOfChild(dialogContainer)
< containerParent.indexOfChild(controlContainer));
});
// When URL bar is not focused, the dialog should be shown on top of the toolbar again.
Espresso.pressBack();
ThreadUtils.runOnUiThreadBlocking(() -> {
Assert.assertTrue(containerParent.indexOfChild(dialogContainer)
> containerParent.indexOfChild(controlContainer));
});
// Dismiss the dialog by clicking OK.
onView(withText(R.string.ok)).perform(click());
}
@Test
@SmallTest
public void testDismiss_ToggleOverview() throws Exception {
// Initially there are no dialogs in the pending list. Browser controls are not restricted.
checkPendingSize(0);
checkBrowserControls(false);
checkCurrentPresenter(null);
// Add two dialogs available for showing.
showDialog(0, ModalDialogManager.TAB_MODAL);
showDialog(1, ModalDialogManager.TAB_MODAL);
checkPendingSize(1);
onView(withId(R.id.tab_modal_dialog_container))
.check(matches(
allOf(hasDescendant(withText("0")), not(hasDescendant(withText("1"))))));
checkBrowserControls(true);
checkCurrentPresenter(ModalDialogManager.TAB_MODAL);
// Dialogs should all be dismissed on entering tab switcher.
onView(withId(R.id.tab_switcher_button)).perform(click());
checkPendingSize(0);
onView(withId(R.id.tab_modal_dialog_container))
.check(matches(allOf(
not(hasDescendant(withText("0"))), not(hasDescendant(withText("1"))))));
checkBrowserControls(false);
checkCurrentPresenter(null);
}
@Test
@SmallTest
public void testDismiss_BackPressed() throws Exception {
// Initially there are no dialogs in the pending list. Browser controls are not restricted.
checkPendingSize(0);
checkBrowserControls(false);
checkCurrentPresenter(null);
// Add two dialogs available for showing.
showDialog(0, ModalDialogManager.TAB_MODAL);
showDialog(1, ModalDialogManager.TAB_MODAL);
showDialog(2, ModalDialogManager.APP_MODAL);
checkPendingSize(2);
onView(withText("2")).check(doesNotExist());
onView(withId(R.id.tab_modal_dialog_container))
.check(matches(allOf(hasDescendant(withText("0")),
not(hasDescendant(withText("1"))), not(hasDescendant(withText("2"))))));
checkBrowserControls(true);
checkCurrentPresenter(ModalDialogManager.TAB_MODAL);
// Perform back press. The first dialog should be dismissed.
// The second dialog should be shown.
Espresso.pressBack();
checkPendingSize(1);
onView(withText("2")).check(doesNotExist());
onView(withId(R.id.tab_modal_dialog_container))
.check(matches(allOf(not(hasDescendant(withText("0"))),
hasDescendant(withText("1")), not(hasDescendant(withText("2"))))));
checkBrowserControls(true);
checkCurrentPresenter(ModalDialogManager.TAB_MODAL);
// Perform a second back press. The second dialog should be dismissed.
// The tab modal container shouldn't be in the window hierarchy when an app modal dialog is
// showing.
Espresso.pressBack();
checkPendingSize(0);
onView(withText("2")).check(matches(isDisplayed()));
onView(withId(R.id.tab_modal_dialog_container)).check(doesNotExist());
checkCurrentPresenter(ModalDialogManager.APP_MODAL);
// Perform a third back press. The third dialog should be dismissed.
Espresso.pressBack();
checkPendingSize(0);
onView(withText("2")).check(doesNotExist());
onView(withId(R.id.tab_modal_dialog_container))
.check(matches(allOf(not(hasDescendant(withText("0"))),
not(hasDescendant(withText("1"))), not(hasDescendant(withText("2"))))));
checkBrowserControls(false);
checkCurrentPresenter(null);
}
private ModalDialogView createDialog(final int index) throws Exception {
return ThreadUtils.runOnUiThreadBlocking(() -> {
ModalDialogView.Controller controller = new ModalDialogView.Controller() {
@Override
public void onCancel() {}
@Override
public void onClick(int buttonType) {
switch (buttonType) {
case ModalDialogView.BUTTON_POSITIVE:
case ModalDialogView.BUTTON_NEGATIVE:
dismissDialog(index);
break;
default:
Assert.fail("Unknown button type: " + buttonType);
}
}
};
final ModalDialogView.Params p = new ModalDialogView.Params();
p.title = Integer.toString(index);
p.positiveButtonTextId = R.string.ok;
p.negativeButtonTextId = R.string.cancel;
return new ModalDialogView(controller, p);
});
}
private void showDialog(
final int index, final @ModalDialogManager.ModalDialogType int dialogType) {
ThreadUtils.runOnUiThreadBlocking(
() -> { mManager.showDialog(mModalDialogViews[index], dialogType); });
}
private void dismissDialog(final int index) {
ThreadUtils.runOnUiThreadBlocking(
() -> { mManager.dismissDialog(mModalDialogViews[index]); });
}
private void checkPendingSize(final int expected) {
ThreadUtils.runOnUiThreadBlocking(() -> {
Assert.assertEquals(expected, mManager.getPendingDialogsForTest().size());
});
}
private void checkCurrentPresenter(final Integer dialogType) {
ThreadUtils.runOnUiThreadBlocking(() -> {
if (dialogType == null) {
Assert.assertFalse(mManager.isShowing());
Assert.assertNull(mManager.getCurrentPresenterForTest());
} else {
Assert.assertTrue(mManager.isShowing());
Assert.assertEquals(mManager.getPresenterForTest(dialogType),
mManager.getCurrentPresenterForTest());
}
});
}
private void checkBrowserControls(boolean restricted) {
if (restricted) {
Assert.assertTrue("All tabs should be obscured", mActivity.isViewObscuringAllTabs());
onView(withId(R.id.menu_button)).check(matches(not(isEnabled())));
} else {
Assert.assertFalse("Tabs shouldn't be obscured", mActivity.isViewObscuringAllTabs());
onView(withId(R.id.menu_button)).check(matches(isEnabled()));
}
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment