| // 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.content.Context; |
| import android.content.DialogInterface; |
| import android.graphics.Color; |
| import android.graphics.drawable.ColorDrawable; |
| import android.text.SpannableString; |
| 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.AdapterView; |
| import android.widget.ArrayAdapter; |
| import android.widget.Button; |
| import android.widget.LinearLayout; |
| import android.widget.ListView; |
| import android.widget.ProgressBar; |
| import android.widget.TextView; |
| |
| import org.chromium.base.ApiCompatibilityUtils; |
| 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.util.HashSet; |
| import java.util.Set; |
| |
| /** |
| * A general-purpose dialog for presenting a list of things to pick from. |
| */ |
| public class ItemChooserDialog { |
| /** |
| * 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); |
| } |
| |
| /** |
| * A class representing one data row in the picker. |
| */ |
| public static class ItemChooserRow { |
| private final String mKey; |
| private final String mDescription; |
| |
| public ItemChooserRow(String key, String description) { |
| mKey = key; |
| mDescription = description; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (!(obj instanceof ItemChooserRow)) return false; |
| if (this == obj) return true; |
| ItemChooserRow item = (ItemChooserRow) obj; |
| return mKey.equals(item.mKey) && mDescription.equals(item.mDescription); |
| } |
| |
| @Override |
| public int hashCode() { |
| return mKey.hashCode() + mDescription.hashCode(); |
| } |
| } |
| |
| /** |
| * The labels to show in the dialog. |
| */ |
| public static class ItemChooserLabels { |
| // The title at the top of the dialog. |
| public final CharSequence mTitle; |
| // The message to show while there are no results. |
| public final CharSequence mSearching; |
| // The message to show when no results were produced. |
| public final CharSequence mNoneFound; |
| // A status message to show above the button row after discovery has |
| // stopped and no devices have been found. |
| public final CharSequence mStatusIdleNoneFound; |
| // A status message to show above the button row after an item has |
| // been added and discovery has stopped. |
| public final CharSequence mStatusIdleSomeFound; |
| // The label for the positive button (e.g. Select/Pair). |
| public final CharSequence mPositiveButton; |
| |
| public ItemChooserLabels(CharSequence title, CharSequence searching, CharSequence noneFound, |
| CharSequence statusIdleNoneFound, CharSequence statusIdleSomeFound, |
| CharSequence positiveButton) { |
| mTitle = title; |
| mSearching = searching; |
| mNoneFound = noneFound; |
| mStatusIdleNoneFound = statusIdleNoneFound; |
| mStatusIdleSomeFound = statusIdleSomeFound; |
| mPositiveButton = positiveButton; |
| } |
| } |
| |
| /** |
| * The various states the dialog can represent. |
| */ |
| private enum State { STARTING, DISCOVERY_IDLE } |
| |
| /** |
| * An adapter for keeping track of which items to show in the dialog. |
| */ |
| private class ItemAdapter extends ArrayAdapter<ItemChooserRow> |
| implements AdapterView.OnItemClickListener { |
| private final LayoutInflater mInflater; |
| |
| // The background color of the highlighted item. |
| private final int mBackgroundHighlightColor; |
| |
| // The color of the non-highlighted text. |
| private final int mDefaultTextColor; |
| |
| // The zero-based index of the item currently selected in the dialog, |
| // or -1 (INVALID_POSITION) if nothing is selected. |
| private int mSelectedItem = ListView.INVALID_POSITION; |
| |
| // A set of keys that are marked as disabled in the dialog. |
| private Set<String> mDisabledEntries = new HashSet<String>(); |
| |
| public ItemAdapter(Context context, int resource) { |
| super(context, resource); |
| |
| mInflater = LayoutInflater.from(context); |
| |
| mBackgroundHighlightColor = ApiCompatibilityUtils.getColor(getContext().getResources(), |
| R.color.light_active_color); |
| mDefaultTextColor = ApiCompatibilityUtils.getColor(getContext().getResources(), |
| R.color.default_text_color); |
| } |
| |
| @Override |
| public void clear() { |
| mSelectedItem = ListView.INVALID_POSITION; |
| mConfirmButton.setEnabled(false); |
| super.clear(); |
| } |
| |
| /** |
| * Returns the key of the currently selected item or blank if nothing is |
| * selected. |
| */ |
| public String getSelectedItemKey() { |
| ItemChooserRow row = getItem(mSelectedItem); |
| if (row == null) return ""; |
| return row.mKey; |
| } |
| |
| /** |
| * Sets whether the itam is enabled. Disabled items are grayed out. |
| * @param id The id of the item to affect. |
| * @param enabled Whether the item should be enabled or not. |
| */ |
| public void setEnabled(String id, boolean enabled) { |
| if (enabled) { |
| mDisabledEntries.remove(id); |
| } else { |
| mDisabledEntries.add(id); |
| } |
| notifyDataSetChanged(); |
| } |
| |
| @Override |
| public boolean isEnabled(int position) { |
| ItemChooserRow item = getItem(position); |
| return !mDisabledEntries.contains(item.mKey); |
| } |
| |
| @Override |
| public int getViewTypeCount() { |
| return 1; |
| } |
| |
| @Override |
| public long getItemId(int position) { |
| return position; |
| } |
| |
| @Override |
| public View getView(int position, View convertView, ViewGroup parent) { |
| TextView view; |
| if (convertView instanceof TextView) { |
| view = (TextView) convertView; |
| } else { |
| view = (TextView) mInflater.inflate( |
| R.layout.item_chooser_dialog_row, parent, false); |
| } |
| |
| // Set highlighting for currently selected item. |
| if (position == mSelectedItem) { |
| view.setBackgroundColor(mBackgroundHighlightColor); |
| view.setTextColor(Color.WHITE); |
| } else { |
| view.setBackground(null); |
| if (!isEnabled(position)) { |
| view.setTextColor(ApiCompatibilityUtils.getColor(getContext().getResources(), |
| R.color.primary_text_disabled_material_light)); |
| } else { |
| view.setTextColor(mDefaultTextColor); |
| } |
| } |
| |
| ItemChooserRow item = getItem(position); |
| view.setText(item.mDescription); |
| return view; |
| } |
| |
| @Override |
| public void onItemClick(AdapterView<?> adapter, View view, int position, long id) { |
| mSelectedItem = position; |
| mConfirmButton.setEnabled(true); |
| mItemAdapter.notifyDataSetChanged(); |
| } |
| } |
| |
| 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 ItemAdapter 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); |
| |
| /** |
| * 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.mTitle); |
| mTitle.setMovementMethod(LinkMovementMethod.getInstance()); |
| |
| mEmptyMessage.setMovementMethod(LinkMovementMethod.getInstance()); |
| mStatus.setMovementMethod(LinkMovementMethod.getInstance()); |
| |
| mConfirmButton = (Button) dialogContainer.findViewById(R.id.positive); |
| mConfirmButton.setText(labels.mPositiveButton); |
| mConfirmButton.setEnabled(false); |
| mConfirmButton.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| mItemSelectedCallback.onItemSelected(mItemAdapter.getSelectedItemKey()); |
| mDialog.setOnDismissListener(null); |
| mDialog.dismiss(); |
| } |
| }); |
| |
| mItemAdapter = new ItemAdapter(mActivity, R.layout.item_chooser_dialog_row); |
| mListView.setAdapter(mItemAdapter); |
| mListView.setEmptyView(mEmptyMessage); |
| mListView.setOnItemClickListener(mItemAdapter); |
| mListView.setDivider(null); |
| setState(State.STARTING); |
| |
| // 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))); |
| |
| showDialogForView(dialogContainer); |
| } |
| |
| // 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 (int) Math.round(heightDp * density); |
| } |
| |
| private void showDialogForView(View view) { |
| mDialog = new Dialog(mActivity); |
| mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); |
| mDialog.addContentView(view, |
| new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, |
| LinearLayout.LayoutParams.MATCH_PARENT)); |
| mDialog.setOnDismissListener(new DialogInterface.OnDismissListener() { |
| @Override |
| public void onDismiss(DialogInterface dialog) { |
| mItemSelectedCallback.onItemSelected(""); |
| } |
| }); |
| |
| Window window = mDialog.getWindow(); |
| if (!DeviceFormFactor.isTablet(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(); |
| } |
| |
| /** |
| * Add an item to the end of the list to show in the dialog. |
| * |
| * @param item The item to be added to the end of the chooser. |
| */ |
| public void addItemToList(ItemChooserRow item) { |
| mProgressBar.setVisibility(View.GONE); |
| mItemAdapter.add(item); |
| setState(State.DISCOVERY_IDLE); |
| } |
| |
| /** |
| * Remove an item that is shown in the dialog. |
| * |
| * @param item The item to be removed in the chooser. |
| */ |
| public void removeItemFromList(ItemChooserRow item) { |
| mItemAdapter.remove(item); |
| 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 id The id of the item to affect. |
| * @param enabled Whether the item should be enabled or not. |
| */ |
| public void setEnabled(String id, boolean enabled) { |
| mItemAdapter.setEnabled(id, enabled); |
| } |
| |
| /** |
| * Clear all items from the dialog. |
| */ |
| public void clear() { |
| mItemAdapter.clear(); |
| setState(State.STARTING); |
| } |
| |
| /** |
| * Shows an error message in the dialog. |
| */ |
| public void setErrorState(SpannableString errorMessage, SpannableString errorStatus) { |
| mListView.setVisibility(View.GONE); |
| mProgressBar.setVisibility(View.GONE); |
| mEmptyMessage.setText(errorMessage); |
| mEmptyMessage.setVisibility(View.VISIBLE); |
| mStatus.setText(errorStatus); |
| } |
| |
| private void setState(State state) { |
| switch (state) { |
| case STARTING: |
| mStatus.setText(mLabels.mSearching); |
| mListView.setVisibility(View.GONE); |
| mProgressBar.setVisibility(View.VISIBLE); |
| mEmptyMessage.setVisibility(View.GONE); |
| break; |
| case DISCOVERY_IDLE: |
| boolean showEmptyMessage = mItemAdapter.isEmpty(); |
| mStatus.setText(showEmptyMessage |
| ? mLabels.mStatusIdleNoneFound : mLabels.mStatusIdleSomeFound); |
| mEmptyMessage.setText(mLabels.mNoneFound); |
| 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 ItemAdapter getItemAdapterForTesting() { |
| return mItemAdapter; |
| } |
| } |