| // 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.chrome.browser; |
| |
| import android.app.Activity; |
| import android.app.Dialog; |
| import android.graphics.Color; |
| import android.graphics.drawable.ColorDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.support.annotation.IntDef; |
| import android.support.annotation.Nullable; |
| import android.text.method.LinkMovementMethod; |
| import android.view.Gravity; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewGroup.LayoutParams; |
| import android.view.Window; |
| import android.widget.Button; |
| import android.widget.LinearLayout; |
| import android.widget.ListView; |
| import android.widget.ProgressBar; |
| import android.widget.TextView; |
| |
| import org.chromium.base.VisibleForTesting; |
| import org.chromium.chrome.R; |
| import org.chromium.chrome.browser.util.MathUtils; |
| import org.chromium.ui.base.DeviceFormFactor; |
| import org.chromium.ui.widget.TextViewWithClickableSpans; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| |
| /** |
| * A general-purpose dialog for presenting a list of things to pick from. |
| * |
| * The dialog is shown by the ItemChooserDialog constructor, and always calls |
| * ItemSelectedCallback.onItemSelected() as it's closing. |
| */ |
| public class ItemChooserDialog implements DeviceItemAdapter.Observer { |
| /** |
| * An interface to implement to get a callback when something has been |
| * selected. |
| */ |
| public interface ItemSelectedCallback { |
| /** |
| * Returns the user selection. |
| * |
| * @param id The id of the item selected. Blank if the dialog was closed |
| * without selecting anything. |
| */ |
| void onItemSelected(String id); |
| } |
| |
| /** |
| * The labels to show in the dialog. |
| */ |
| public static class ItemChooserLabels { |
| // The title at the top of the dialog. |
| public final CharSequence title; |
| // The message to show while there are no results. |
| public final CharSequence searching; |
| // The message to show when no results were produced. |
| public final CharSequence noneFound; |
| // A status message to show above the button row after an item has |
| // been added and discovery is still ongoing. |
| public final CharSequence statusActive; |
| // A status message to show above the button row after discovery has |
| // stopped and no devices have been found. |
| public final CharSequence statusIdleNoneFound; |
| // A status message to show above the button row after an item has |
| // been added and discovery has stopped. |
| public final CharSequence statusIdleSomeFound; |
| // The label for the positive button (e.g. Select/Pair). |
| public final CharSequence positiveButton; |
| |
| public ItemChooserLabels(CharSequence title, CharSequence searching, CharSequence noneFound, |
| CharSequence statusActive, CharSequence statusIdleNoneFound, |
| CharSequence statusIdleSomeFound, CharSequence positiveButton) { |
| this.title = title; |
| this.searching = searching; |
| this.noneFound = noneFound; |
| this.statusActive = statusActive; |
| this.statusIdleNoneFound = statusIdleNoneFound; |
| this.statusIdleSomeFound = statusIdleSomeFound; |
| this.positiveButton = positiveButton; |
| } |
| } |
| |
| /** |
| * The various states the dialog can represent. |
| */ |
| @IntDef({State.INITIALIZING_ADAPTER, State.STARTING, State.PROGRESS_UPDATE_AVAILABLE, |
| State.DISCOVERY_IDLE}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface State { |
| int INITIALIZING_ADAPTER = 0; |
| int STARTING = 1; |
| int PROGRESS_UPDATE_AVAILABLE = 2; |
| int DISCOVERY_IDLE = 3; |
| } |
| |
| private Activity mActivity; |
| |
| // The dialog this class encapsulates. |
| private Dialog mDialog; |
| |
| // The callback to notify when the user selected an item. |
| private ItemSelectedCallback mItemSelectedCallback; |
| |
| // Individual UI elements. |
| private TextViewWithClickableSpans mTitle; |
| private TextViewWithClickableSpans mEmptyMessage; |
| private ProgressBar mProgressBar; |
| private ListView mListView; |
| private TextView mStatus; |
| private Button mConfirmButton; |
| |
| // The labels to display in the dialog. |
| private ItemChooserLabels mLabels; |
| |
| // The adapter containing the items to show in the dialog. |
| private DeviceItemAdapter mItemAdapter; |
| |
| // How much of the height of the screen should be taken up by the listview. |
| private static final float LISTVIEW_HEIGHT_PERCENT = 0.30f; |
| // The height of a row of the listview in dp. |
| private static final int LIST_ROW_HEIGHT_DP = 48; |
| // The minimum height of the listview in the dialog (in dp). |
| private static final int MIN_HEIGHT_DP = (int) (LIST_ROW_HEIGHT_DP * 1.5); |
| // The maximum height of the listview in the dialog (in dp). |
| private static final int MAX_HEIGHT_DP = (int) (LIST_ROW_HEIGHT_DP * 8.5); |
| |
| // If this variable is false, the window should be closed when it loses focus; |
| // Otherwise, the window should not be closed when it loses focus. |
| private boolean mIgnorePendingWindowFocusChangeForClose; |
| |
| /** |
| * Creates the ItemChooserPopup and displays it (and starts waiting for data). |
| * |
| * @param activity Activity which is used for launching a dialog. |
| * @param callback The callback used to communicate back what was selected. |
| * @param labels The labels to show in the dialog. |
| */ |
| public ItemChooserDialog( |
| Activity activity, ItemSelectedCallback callback, ItemChooserLabels labels) { |
| mActivity = activity; |
| mItemSelectedCallback = callback; |
| mLabels = labels; |
| |
| LinearLayout dialogContainer = (LinearLayout) LayoutInflater.from(mActivity).inflate( |
| R.layout.item_chooser_dialog, null); |
| |
| mListView = (ListView) dialogContainer.findViewById(R.id.items); |
| mProgressBar = (ProgressBar) dialogContainer.findViewById(R.id.progress); |
| mStatus = (TextView) dialogContainer.findViewById(R.id.status); |
| mTitle = (TextViewWithClickableSpans) dialogContainer.findViewById( |
| R.id.dialog_title); |
| mEmptyMessage = |
| (TextViewWithClickableSpans) dialogContainer.findViewById(R.id.not_found_message); |
| |
| mTitle.setText(labels.title); |
| mTitle.setMovementMethod(LinkMovementMethod.getInstance()); |
| |
| mEmptyMessage.setMovementMethod(LinkMovementMethod.getInstance()); |
| mStatus.setMovementMethod(LinkMovementMethod.getInstance()); |
| |
| mConfirmButton = (Button) dialogContainer.findViewById(R.id.positive); |
| mConfirmButton.setText(labels.positiveButton); |
| mConfirmButton.setEnabled(false); |
| mConfirmButton.setOnClickListener(v -> { |
| mItemSelectedCallback.onItemSelected(mItemAdapter.getSelectedItemKey()); |
| mDialog.setOnDismissListener(null); |
| mDialog.dismiss(); |
| }); |
| |
| mItemAdapter = new DeviceItemAdapter(mActivity, R.layout.item_chooser_dialog_row); |
| mItemAdapter.setNotifyOnChange(true); |
| mItemAdapter.setObserver(this); |
| mListView.setAdapter(mItemAdapter); |
| mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); |
| mListView.setEmptyView(mEmptyMessage); |
| mListView.setOnItemClickListener(mItemAdapter); |
| mListView.setDivider(null); |
| setState(State.STARTING); |
| |
| mIgnorePendingWindowFocusChangeForClose = false; |
| |
| showDialogForView(dialogContainer); |
| |
| dialogContainer.addOnLayoutChangeListener( |
| (View v, int l, int t, int r, int b, int ol, int ot, int or, int ob) -> { |
| if (l != ol || t != ot || r != or || b != ob) { |
| // The list is the main element in the dialog and it should grow and |
| // shrink according to the size of the screen available. |
| View listViewContainer = dialogContainer.findViewById(R.id.container); |
| listViewContainer.setLayoutParams(new LinearLayout.LayoutParams( |
| LayoutParams.MATCH_PARENT, |
| getListHeight(mActivity.getWindow().getDecorView().getHeight(), |
| mActivity.getResources().getDisplayMetrics().density))); |
| } |
| }); |
| } |
| |
| // DeviceItemAdapter.Observer: |
| @Override |
| public void onItemSelectionChanged(boolean itemSelected) { |
| mConfirmButton.setEnabled(itemSelected); |
| } |
| |
| /** |
| * Sets whether the window should be closed when it loses focus. |
| * |
| * @param ignorePendingWindowFocusChangeForClose Whether the window should be closed when it |
| * loses focus. |
| */ |
| public void setIgnorePendingWindowFocusChangeForClose( |
| boolean ignorePendingWindowFocusChangeForClose) { |
| mIgnorePendingWindowFocusChangeForClose = ignorePendingWindowFocusChangeForClose; |
| } |
| |
| // Computes the height of the device list, bound to half-multiples of the |
| // row height so that it's obvious if there are more elements to scroll to. |
| @VisibleForTesting |
| static int getListHeight(int decorHeight, float density) { |
| float heightDp = decorHeight / density * LISTVIEW_HEIGHT_PERCENT; |
| // Round to (an integer + 0.5) times LIST_ROW_HEIGHT. |
| heightDp = (Math.round(heightDp / LIST_ROW_HEIGHT_DP - 0.5f) + 0.5f) * LIST_ROW_HEIGHT_DP; |
| heightDp = MathUtils.clamp(heightDp, MIN_HEIGHT_DP, MAX_HEIGHT_DP); |
| return Math.round(heightDp * density); |
| } |
| |
| private void showDialogForView(View view) { |
| mDialog = new Dialog(mActivity) { |
| @Override |
| public void onWindowFocusChanged(boolean hasFocus) { |
| super.onWindowFocusChanged(hasFocus); |
| if (!mIgnorePendingWindowFocusChangeForClose && !hasFocus) super.dismiss(); |
| setIgnorePendingWindowFocusChangeForClose(false); |
| } |
| }; |
| mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); |
| mDialog.setCanceledOnTouchOutside(true); |
| mDialog.addContentView(view, |
| new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, |
| LinearLayout.LayoutParams.MATCH_PARENT)); |
| mDialog.setOnDismissListener(dialog -> mItemSelectedCallback.onItemSelected("")); |
| |
| Window window = mDialog.getWindow(); |
| if (!DeviceFormFactor.isNonMultiDisplayContextOnTablet(mActivity)) { |
| // On smaller screens, make the dialog fill the width of the screen, |
| // and appear at the top. |
| window.setBackgroundDrawable(new ColorDrawable(Color.WHITE)); |
| window.setGravity(Gravity.TOP); |
| window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, |
| ViewGroup.LayoutParams.WRAP_CONTENT); |
| } |
| |
| mDialog.show(); |
| } |
| |
| public void dismiss() { |
| mDialog.dismiss(); |
| } |
| |
| /** |
| * Adds an item to the end of the list to show in the dialog if the item |
| * was not in the chooser. Otherwise updates the items description. |
| * |
| * @param key Unique identifier for that item. |
| * @param description Text in the row. |
| */ |
| public void addOrUpdateItem(String key, String description) { |
| addOrUpdateItem(key, description, null /* icon */, null /* iconDescription */); |
| } |
| |
| /** |
| * Adds an item to the end of the list to show in the dialog if the item |
| * was not in the chooser. Otherwise updates the items description or icon. |
| * Note that as long as at least one item has an icon all rows will be inset |
| * with the icon dimensions. |
| * |
| * @param key Unique identifier for that item. |
| * @param description Text in the row. |
| * @param icon Drawable to show left of the description. The drawable provided should |
| * be stateful and handle the selected state to be rendered correctly. |
| * @param iconDescription Description of the icon. |
| */ |
| public void addOrUpdateItem(String key, String description, @Nullable Drawable icon, |
| @Nullable String iconDescription) { |
| mProgressBar.setVisibility(View.GONE); |
| mItemAdapter.addOrUpdate(key, description, icon, iconDescription); |
| setState(State.PROGRESS_UPDATE_AVAILABLE); |
| } |
| |
| /** |
| * Removes an item that is shown in the dialog. |
| * |
| * @param key Unique identifier for the item. |
| */ |
| public void removeItemFromList(String key) { |
| mItemAdapter.removeItemWithKey(key); |
| setState(State.DISCOVERY_IDLE); |
| } |
| |
| /** |
| * Indicates the chooser that no more items will be added. |
| */ |
| public void setIdleState() { |
| mProgressBar.setVisibility(View.GONE); |
| setState(State.DISCOVERY_IDLE); |
| } |
| |
| /** |
| * Sets whether the item is enabled. |
| * @param key Unique indetifier for the item. |
| * @param enabled Whether the item should be enabled or not. |
| */ |
| public void setEnabled(String key, boolean enabled) { |
| mItemAdapter.setEnabled(key, enabled); |
| } |
| |
| /** |
| * Indicates the adapter is being initialized. |
| */ |
| public void signalInitializingAdapter() { |
| setState(State.INITIALIZING_ADAPTER); |
| } |
| |
| /** |
| * Clear all items from the dialog. |
| */ |
| public void clear() { |
| mItemAdapter.clear(); |
| setState(State.STARTING); |
| } |
| |
| /** |
| * Shows an error message in the dialog. |
| */ |
| public void setErrorState(CharSequence errorMessage, CharSequence errorStatus) { |
| mListView.setVisibility(View.GONE); |
| mProgressBar.setVisibility(View.GONE); |
| mEmptyMessage.setText(errorMessage); |
| mEmptyMessage.setVisibility(View.VISIBLE); |
| mStatus.setText(errorStatus); |
| } |
| |
| private void setState(@State int state) { |
| switch (state) { |
| case State.STARTING: |
| mStatus.setText(mLabels.searching); |
| // fall through |
| case State.INITIALIZING_ADAPTER: |
| mListView.setVisibility(View.GONE); |
| mProgressBar.setVisibility(View.VISIBLE); |
| mEmptyMessage.setVisibility(View.GONE); |
| break; |
| case State.PROGRESS_UPDATE_AVAILABLE: |
| mStatus.setText(mLabels.statusActive); |
| mProgressBar.setVisibility(View.GONE); |
| mListView.setVisibility(View.VISIBLE); |
| break; |
| case State.DISCOVERY_IDLE: |
| boolean showEmptyMessage = mItemAdapter.isEmpty(); |
| mStatus.setText(showEmptyMessage |
| ? mLabels.statusIdleNoneFound : mLabels.statusIdleSomeFound); |
| mEmptyMessage.setText(mLabels.noneFound); |
| mEmptyMessage.setVisibility(showEmptyMessage ? View.VISIBLE : View.GONE); |
| break; |
| } |
| } |
| |
| /** |
| * Returns the dialog associated with this class. For use with tests only. |
| */ |
| @VisibleForTesting |
| public Dialog getDialogForTesting() { |
| return mDialog; |
| } |
| |
| /** |
| * Returns the ItemAdapter associated with this class. For use with tests only. |
| */ |
| @VisibleForTesting |
| public DeviceItemAdapter getItemAdapterForTesting() { |
| return mItemAdapter; |
| } |
| } |