michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.widget; michael@0: michael@0: import org.mozilla.gecko.R; michael@0: import org.mozilla.gecko.Tabs; michael@0: import org.mozilla.gecko.prompts.PromptInput; michael@0: michael@0: import org.json.JSONArray; michael@0: import org.json.JSONException; michael@0: import org.json.JSONObject; michael@0: michael@0: import android.content.Context; michael@0: import android.content.res.Resources; michael@0: import android.graphics.Rect; michael@0: import android.os.Build; michael@0: import android.text.SpannableString; michael@0: import android.text.TextUtils; michael@0: import android.text.method.LinkMovementMethod; michael@0: import android.text.style.ForegroundColorSpan; michael@0: import android.text.style.URLSpan; michael@0: import android.util.Log; michael@0: import android.view.LayoutInflater; michael@0: import android.view.View; michael@0: import android.view.ViewGroup; michael@0: import android.widget.Button; michael@0: import android.widget.CheckBox; michael@0: import android.widget.ImageView; michael@0: import android.widget.LinearLayout; michael@0: import android.widget.Spinner; michael@0: import android.widget.SpinnerAdapter; michael@0: import android.widget.TextView; michael@0: michael@0: import java.util.ArrayList; michael@0: import java.util.List; michael@0: michael@0: public class DoorHanger extends LinearLayout { michael@0: private static final String LOGTAG = "GeckoDoorHanger"; michael@0: michael@0: private static int sInputPadding = -1; michael@0: private static int sSpinnerTextColor = -1; michael@0: private static int sSpinnerTextSize = -1; michael@0: michael@0: private static LayoutParams sButtonParams; michael@0: static { michael@0: sButtonParams = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT, 1.0f); michael@0: } michael@0: michael@0: private final TextView mTextView; michael@0: private final ImageView mIcon; michael@0: private final LinearLayout mChoicesLayout; michael@0: michael@0: // Divider between doorhangers. michael@0: private final View mDivider; michael@0: michael@0: // The tab associated with this notification. michael@0: private final int mTabId; michael@0: michael@0: // Value used to identify the notification. michael@0: private final String mValue; michael@0: michael@0: private Resources mResources; michael@0: michael@0: private List mInputs; michael@0: private CheckBox mCheckBox; michael@0: michael@0: private int mPersistence = 0; michael@0: private boolean mPersistWhileVisible = false; michael@0: private long mTimeout = 0; michael@0: michael@0: // Color used for dividers above and between buttons. michael@0: private int mDividerColor; michael@0: michael@0: public static enum Theme { michael@0: LIGHT, michael@0: DARK michael@0: } michael@0: michael@0: public interface OnButtonClickListener { michael@0: public void onButtonClick(DoorHanger dh, String tag); michael@0: } michael@0: michael@0: public DoorHanger(Context context, Theme theme) { michael@0: this(context, 0, null, theme); michael@0: } michael@0: michael@0: public DoorHanger(Context context, int tabId, String value) { michael@0: this(context, tabId, value, Theme.LIGHT); michael@0: } michael@0: michael@0: private DoorHanger(Context context, int tabId, String value, Theme theme) { michael@0: super(context); michael@0: michael@0: mTabId = tabId; michael@0: mValue = value; michael@0: mResources = getResources(); michael@0: michael@0: if (sInputPadding == -1) { michael@0: sInputPadding = mResources.getDimensionPixelSize(R.dimen.doorhanger_padding); michael@0: } michael@0: if (sSpinnerTextColor == -1) { michael@0: sSpinnerTextColor = mResources.getColor(R.color.text_color_primary_disable_only); michael@0: } michael@0: if (sSpinnerTextSize == -1) { michael@0: sSpinnerTextSize = mResources.getDimensionPixelSize(R.dimen.doorhanger_spinner_textsize); michael@0: } michael@0: michael@0: setOrientation(VERTICAL); michael@0: michael@0: LayoutInflater.from(context).inflate(R.layout.doorhanger, this); michael@0: mTextView = (TextView) findViewById(R.id.doorhanger_title); michael@0: mIcon = (ImageView) findViewById(R.id.doorhanger_icon); michael@0: mChoicesLayout = (LinearLayout) findViewById(R.id.doorhanger_choices); michael@0: mDivider = findViewById(R.id.divider_doorhanger); michael@0: michael@0: setTheme(theme); michael@0: } michael@0: michael@0: private void setTheme(Theme theme) { michael@0: if (theme == Theme.LIGHT) { michael@0: // The default styles declared in doorhanger.xml are light-themed, so we just michael@0: // need to set the divider color that we'll use in addButton. michael@0: mDividerColor = mResources.getColor(R.color.doorhanger_divider_light); michael@0: michael@0: } else if (theme == Theme.DARK) { michael@0: mDividerColor = mResources.getColor(R.color.doorhanger_divider_dark); michael@0: michael@0: // Set a dark background, and use a smaller text size for dark-themed DoorHangers. michael@0: setBackgroundColor(mResources.getColor(R.color.doorhanger_background_dark)); michael@0: mTextView.setTextSize(mResources.getDimension(R.dimen.doorhanger_textsize_small)); michael@0: } michael@0: } michael@0: michael@0: public int getTabId() { michael@0: return mTabId; michael@0: } michael@0: michael@0: public String getValue() { michael@0: return mValue; michael@0: } michael@0: michael@0: public List getInputs() { michael@0: return mInputs; michael@0: } michael@0: michael@0: public CheckBox getCheckBox() { michael@0: return mCheckBox; michael@0: } michael@0: michael@0: public void showDivider() { michael@0: mDivider.setVisibility(View.VISIBLE); michael@0: } michael@0: michael@0: public void hideDivider() { michael@0: mDivider.setVisibility(View.GONE); michael@0: } michael@0: michael@0: public void setMessage(String message) { michael@0: mTextView.setText(message); michael@0: } michael@0: michael@0: public void setIcon(int resId) { michael@0: mIcon.setImageResource(resId); michael@0: mIcon.setVisibility(View.VISIBLE); michael@0: } michael@0: michael@0: public void addLink(String label, String url, String delimiter) { michael@0: String title = mTextView.getText().toString(); michael@0: SpannableString titleWithLink = new SpannableString(title + delimiter + label); michael@0: URLSpan linkSpan = new URLSpan(url) { michael@0: @Override michael@0: public void onClick(View view) { michael@0: Tabs.getInstance().loadUrlInTab(getURL()); michael@0: } michael@0: }; michael@0: michael@0: // Prevent text outside the link from flashing when clicked. michael@0: ForegroundColorSpan colorSpan = new ForegroundColorSpan(mTextView.getCurrentTextColor()); michael@0: titleWithLink.setSpan(colorSpan, 0, title.length(), 0); michael@0: michael@0: titleWithLink.setSpan(linkSpan, title.length() + 1, titleWithLink.length(), 0); michael@0: mTextView.setText(titleWithLink); michael@0: mTextView.setMovementMethod(LinkMovementMethod.getInstance()); michael@0: } michael@0: michael@0: public void addButton(final String text, final String tag, final OnButtonClickListener listener) { michael@0: final Button button = (Button) LayoutInflater.from(getContext()).inflate(R.layout.doorhanger_button, null); michael@0: button.setText(text); michael@0: button.setTag(tag); michael@0: michael@0: button.setOnClickListener(new Button.OnClickListener() { michael@0: @Override michael@0: public void onClick(View v) { michael@0: listener.onButtonClick(DoorHanger.this, tag); michael@0: } michael@0: }); michael@0: michael@0: if (mChoicesLayout.getChildCount() == 0) { michael@0: // If this is the first button we're adding, make the choices layout visible. michael@0: mChoicesLayout.setVisibility(View.VISIBLE); michael@0: // Make the divider above the buttons visible. michael@0: View divider = findViewById(R.id.divider_choices); michael@0: divider.setVisibility(View.VISIBLE); michael@0: divider.setBackgroundColor(mDividerColor); michael@0: } else { michael@0: // Add a vertical divider between additional buttons. michael@0: Divider divider = new Divider(getContext(), null); michael@0: divider.setOrientation(Divider.Orientation.VERTICAL); michael@0: divider.setBackgroundColor(mDividerColor); michael@0: mChoicesLayout.addView(divider); michael@0: } michael@0: michael@0: mChoicesLayout.addView(button, sButtonParams); michael@0: } michael@0: michael@0: public void setOptions(final JSONObject options) { michael@0: final int persistence = options.optInt("persistence"); michael@0: if (persistence > 0) { michael@0: mPersistence = persistence; michael@0: } michael@0: michael@0: mPersistWhileVisible = options.optBoolean("persistWhileVisible"); michael@0: michael@0: final long timeout = options.optLong("timeout"); michael@0: if (timeout > 0) { michael@0: mTimeout = timeout; michael@0: } michael@0: michael@0: final JSONObject link = options.optJSONObject("link"); michael@0: if (link != null) { michael@0: try { michael@0: final String linkLabel = link.getString("label"); michael@0: final String linkUrl = link.getString("url"); michael@0: addLink(linkLabel, linkUrl, " "); michael@0: } catch (JSONException e) { } michael@0: } michael@0: michael@0: final JSONArray inputs = options.optJSONArray("inputs"); michael@0: if (inputs != null) { michael@0: mInputs = new ArrayList(); michael@0: michael@0: final ViewGroup group = (ViewGroup) findViewById(R.id.doorhanger_inputs); michael@0: group.setVisibility(VISIBLE); michael@0: michael@0: for (int i = 0; i < inputs.length(); i++) { michael@0: try { michael@0: PromptInput input = PromptInput.getInput(inputs.getJSONObject(i)); michael@0: mInputs.add(input); michael@0: michael@0: View v = input.getView(getContext()); michael@0: styleInput(input, v); michael@0: group.addView(v); michael@0: } catch(JSONException ex) { } michael@0: } michael@0: } michael@0: michael@0: final String checkBoxText = options.optString("checkbox"); michael@0: if (!TextUtils.isEmpty(checkBoxText)) { michael@0: mCheckBox = (CheckBox) findViewById(R.id.doorhanger_checkbox); michael@0: mCheckBox.setText(checkBoxText); michael@0: mCheckBox.setVisibility(VISIBLE); michael@0: } michael@0: } michael@0: michael@0: private void styleInput(PromptInput input, View view) { michael@0: if (input instanceof PromptInput.MenulistInput) { michael@0: styleSpinner(input, view); michael@0: } else { michael@0: // add some top and bottom padding to separate inputs michael@0: view.setPadding(0, sInputPadding, michael@0: 0, sInputPadding); michael@0: } michael@0: } michael@0: michael@0: private void styleSpinner(PromptInput input, View view) { michael@0: PromptInput.MenulistInput spinInput = (PromptInput.MenulistInput) input; michael@0: michael@0: /* Spinners have some intrinsic padding. To force the spinner's text to line up with michael@0: * the doorhanger text, we have to take that padding into account. michael@0: * michael@0: * |-----A-------| <-- Normal doorhanger message michael@0: * |-B-|---C+D---| <-- (optional) Spinner Label michael@0: * |-B-|-C-|--D--| <-- Spinner michael@0: * michael@0: * A - Desired padding (sInputPadding) michael@0: * B - Final padding applied to input element (sInputPadding - rect.left - textPadding). michael@0: * C - Spinner background drawable padding (rect.left). michael@0: * D - Spinner inner TextView padding (textPadding). michael@0: */ michael@0: michael@0: // First get the padding of the selected view inside the spinner. Since the spinner michael@0: // hasn't been shown yet, we get this view directly from the adapter. michael@0: Spinner spinner = spinInput.spinner; michael@0: SpinnerAdapter adapter = spinner.getAdapter(); michael@0: View dropView = adapter.getView(0, null, spinner); michael@0: int textPadding = 0; michael@0: if (dropView != null) { michael@0: textPadding = dropView.getPaddingLeft(); michael@0: } michael@0: michael@0: // Then get the intrinsic padding built into the background image of the spinner. michael@0: Rect rect = new Rect(); michael@0: spinner.getBackground().getPadding(rect); michael@0: michael@0: // Set the difference in padding to the spinner view to align it with doorhanger text. michael@0: view.setPadding(sInputPadding - rect.left - textPadding, 0, rect.right, sInputPadding); michael@0: michael@0: if (spinInput.textView != null) { michael@0: spinInput.textView.setTextColor(sSpinnerTextColor); michael@0: spinInput.textView.setTextSize(sSpinnerTextSize); michael@0: michael@0: // If this spinner has a label, offset it to also be aligned with the doorhanger text. michael@0: spinInput.textView.setPadding(rect.left + textPadding, 0, 0, 0); michael@0: } michael@0: } michael@0: michael@0: michael@0: /* michael@0: * Checks with persistence and timeout options to see if it's okay to remove a doorhanger. michael@0: * michael@0: * @param isShowing Whether or not this doorhanger is currently visible to the user. michael@0: * (e.g. the DoorHanger view might be VISIBLE, but its parent could be hidden) michael@0: */ michael@0: public boolean shouldRemove(boolean isShowing) { michael@0: if (mPersistWhileVisible && isShowing) { michael@0: // We still want to decrement mPersistence, even if the popup is showing michael@0: if (mPersistence != 0) michael@0: mPersistence--; michael@0: return false; michael@0: } michael@0: michael@0: // If persistence is set to -1, the doorhanger will never be michael@0: // automatically removed. michael@0: if (mPersistence != 0) { michael@0: mPersistence--; michael@0: return false; michael@0: } michael@0: michael@0: if (System.currentTimeMillis() <= mTimeout) { michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: }