blob: 5ed0af8c875890a5e774715c15a092c313440076 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.touchless.dialog;
import android.app.Dialog;
import android.graphics.drawable.Drawable;
import android.support.v4.view.ViewCompat;
import android.util.Pair;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.ListView;
import android.widget.TextView;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.touchless.TouchlessModelCoordinator;
import org.chromium.chrome.browser.touchless.dialog.TouchlessDialogProperties.DialogListItemProperties;
import org.chromium.chrome.browser.touchless.dialog.TouchlessDialogProperties.ListItemType;
import org.chromium.chrome.touchless.R;
import org.chromium.ui.modaldialog.DialogDismissalCause;
import org.chromium.ui.modaldialog.ModalDialogManager.Presenter;
import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modelutil.ModelListAdapter;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
import org.chromium.ui.widget.ChromeImageView;
import java.util.ArrayList;
/** A modal dialog presenter that is specific to touchless dialogs. */
public class TouchlessDialogPresenter extends Presenter {
/** An activity to attach dialogs to. */
private final ChromeActivity mActivity;
private final TouchlessModelCoordinator mModelCoordinator;
/** The dialog this class abstracts. */
private Dialog mDialog;
/** The model change processor for the currently shown dialog. */
private PropertyModelChangeProcessor<PropertyModel, Pair<ViewGroup, ModelListAdapter>,
PropertyKey> mModelChangeProcessor;
public TouchlessDialogPresenter(
ChromeActivity activity, TouchlessModelCoordinator modelCoordinator) {
mActivity = activity;
mModelCoordinator = modelCoordinator;
}
@Override
protected void addDialogView(PropertyModel model) {
// If the activity's decor view is not attached to window, we don't show the dialog because
// the window manager might have revoked the window token for this activity. See
// https://crbug.com/926688.
Window window = mActivity.getWindow();
if (window == null || !ViewCompat.isAttachedToWindow(window.getDecorView())) {
dismissCurrentDialog(DialogDismissalCause.NOT_ATTACHED_TO_WINDOW);
return;
}
// If not fullscreen don't use a predefined style.
if (!model.get(TouchlessDialogProperties.IS_FULLSCREEN)) {
mDialog = new Dialog(mActivity);
mDialog.getWindow().setGravity(Gravity.BOTTOM);
mDialog.getWindow().setBackgroundDrawable(ApiCompatibilityUtils.getDrawable(
mActivity.getResources(), R.drawable.dialog_rounded_top));
} else {
mDialog = new Dialog(mActivity, R.style.Theme_Chromium_DialogWhenLarge);
}
mDialog.setOnCancelListener(dialogInterface
-> dismissCurrentDialog(DialogDismissalCause.NAVIGATE_BACK_OR_TOUCH_OUTSIDE));
mDialog.setOnShowListener(dialog -> {
if (mModelCoordinator != null) mModelCoordinator.addModelToQueue(model);
});
mDialog.setOnDismissListener(dialog -> {
if (mModelCoordinator != null) mModelCoordinator.removeModelFromQueue(model);
});
// Cancel on touch outside should be disabled by default. The ModelChangeProcessor wouldn't
// notify change if the property is not set during initialization.
mDialog.setCanceledOnTouchOutside(false);
mDialog.setOnKeyListener((dialog, keyCode, event) -> {
return mModelCoordinator != null ? (mModelCoordinator.onKeyEvent(event) == null)
: false;
});
ViewGroup dialogView = (ViewGroup) LayoutInflater.from(mDialog.getContext())
.inflate(R.layout.touchless_dialog_view, null);
ModelListAdapter adapter = new ModelListAdapter();
adapter.registerType(ListItemType.DEFAULT,
() -> LayoutInflater.from(mActivity).inflate(R.layout.dialog_list_item, null),
TouchlessDialogPresenter::bindListItem);
ListView dialogOptions = dialogView.findViewById(R.id.touchless_dialog_option_list);
dialogOptions.setAdapter(adapter);
dialogOptions.setItemsCanFocus(true);
mModelChangeProcessor = PropertyModelChangeProcessor.create(
model, Pair.create(dialogView, adapter), TouchlessDialogPresenter::bind);
mDialog.setContentView(dialogView);
// If the modal dialog is not specified to be fullscreen, wrap content and place at the
// bottom of the screen. This needs to be done after content is added to the dialog.
if (!model.get(TouchlessDialogProperties.IS_FULLSCREEN)) {
mDialog.getWindow().setLayout(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
mDialog.show();
dialogView.announceForAccessibility(getContentDescription(model));
}
@Override
protected void removeDialogView(PropertyModel model) {
if (mModelChangeProcessor != null) {
mModelChangeProcessor.destroy();
mModelChangeProcessor = null;
}
if (mDialog != null) {
mDialog.dismiss();
mDialog = null;
}
}
/**
* Bind a model to a touchless dialog view.
* @param model The model being bound.
* @param view The view to apply the model to.
* @param propertyKey The property that changed.
*/
private static void bind(
PropertyModel model, Pair<ViewGroup, ModelListAdapter> view, PropertyKey propertyKey) {
ViewGroup dialogView = view.first;
ModelListAdapter optionsAdapter = view.second;
// TODO(mdjones): If the default buttons are used assert no list items and convert the
// buttons to list items.
if (TouchlessDialogProperties.IS_FULLSCREEN == propertyKey) {
// TODO(mdjones): Implement fullscreen/non-fullscreen modes.
} else if (ModalDialogProperties.TITLE_ICON == propertyKey) {
ChromeImageView imageView = dialogView.findViewById(R.id.touchless_dialog_icon);
imageView.setImageDrawable(model.get(ModalDialogProperties.TITLE_ICON));
imageView.setVisibility(View.VISIBLE);
} else if (ModalDialogProperties.TITLE == propertyKey) {
TextView textView = dialogView.findViewById(R.id.touchless_dialog_title);
textView.setText(model.get(ModalDialogProperties.TITLE));
textView.setVisibility(View.VISIBLE);
} else if (ModalDialogProperties.MESSAGE == propertyKey) {
TextView textView = dialogView.findViewById(R.id.touchless_dialog_description);
textView.setText(model.get(ModalDialogProperties.MESSAGE));
textView.setVisibility(View.VISIBLE);
} else if (ModalDialogProperties.CUSTOM_VIEW == propertyKey) {
ViewGroup customGroup = dialogView.findViewById(R.id.custom);
customGroup.addView(model.get(ModalDialogProperties.CUSTOM_VIEW));
customGroup.setVisibility(View.VISIBLE);
} else if (TouchlessDialogProperties.LIST_MODELS == propertyKey) {
PropertyModel[] models = model.get(TouchlessDialogProperties.LIST_MODELS);
ArrayList<Pair<Integer, PropertyModel>> modelPairs = new ArrayList<>();
for (int i = 0; i < models.length; i++) {
modelPairs.add(Pair.create(0, models[i]));
}
optionsAdapter.updateModels(modelPairs);
} else if (TouchlessDialogProperties.FORCE_SINGLE_LINE_TITLE == propertyKey) {
TextView textView = dialogView.findViewById(R.id.touchless_dialog_title);
textView.setMaxLines(model.get(TouchlessDialogProperties.FORCE_SINGLE_LINE_TITLE)
? 1
: Integer.MAX_VALUE);
}
}
/**
* Bind a list model item to its view.
* @param model The model to bind.
* @param view The view to be bound to.
* @param propertyKey The property that changed.
*/
private static void bindListItem(PropertyModel model, View view, PropertyKey propertyKey) {
ChromeImageView imageView = view.findViewById(R.id.dialog_item_icon);
TextView textView = view.findViewById(R.id.dialog_item_text);
if (DialogListItemProperties.ICON == propertyKey) {
if (model.get(DialogListItemProperties.ICON) == null) {
imageView.setImageDrawable(null);
} else {
imageView.setVisibility(View.VISIBLE);
Drawable icon = model.get(DialogListItemProperties.ICON).mutate();
icon.clearColorFilter();
imageView.setImageDrawable(icon);
}
} else if (DialogListItemProperties.TEXT == propertyKey) {
textView.setText(model.get(DialogListItemProperties.TEXT));
} else if (DialogListItemProperties.CLICK_LISTENER == propertyKey) {
if (!(model.get(DialogListItemProperties.CLICK_LISTENER)
instanceof ClickThrottlingListener)) {
ClickThrottlingListener listener = new ClickThrottlingListener(
model.get(DialogListItemProperties.CLICK_LISTENER));
listener.setIsMultiClickable(model.get(DialogListItemProperties.MULTI_CLICKABLE));
model.set(DialogListItemProperties.CLICK_LISTENER, listener);
view.setOnClickListener(listener);
} else {
ClickThrottlingListener listener = (ClickThrottlingListener) model.get(
DialogListItemProperties.CLICK_LISTENER);
listener.resetWasClicked();
view.setOnClickListener(listener);
}
} else if (DialogListItemProperties.MULTI_CLICKABLE == propertyKey) {
View.OnClickListener listener = model.get(DialogListItemProperties.CLICK_LISTENER);
if (listener instanceof ClickThrottlingListener) {
((ClickThrottlingListener) listener)
.setIsMultiClickable(model.get(DialogListItemProperties.MULTI_CLICKABLE));
}
}
}
/**
* This OnClickListener implementation ensures that
* {@link DialogListItemProperties.CLICK_LISTENER} is only called once for models with
* {@link DialogListItemProperties.MULTI_CLICKABLE} set to false.
*/
private static class ClickThrottlingListener implements View.OnClickListener {
private View.OnClickListener mOnClickListener;
private boolean mWasClicked;
private boolean mIsMultiClickable;
private ClickThrottlingListener(View.OnClickListener onClickListener) {
mOnClickListener = onClickListener;
}
private void setIsMultiClickable(boolean isMultiClickable) {
mIsMultiClickable = isMultiClickable;
}
private void resetWasClicked() {
mWasClicked = false;
}
@Override
public void onClick(View view) {
if (mOnClickListener == null) return;
if (!mIsMultiClickable && mWasClicked) return;
mOnClickListener.onClick(view);
mWasClicked = true;
}
}
}