Bug 1144385 - Refactor buttons into DoorHanger. r=margaret
authorChenxia Liu <liuche@mozilla.com>
Mon, 30 Mar 2015 19:26:14 -0700
changeset 238167 cb1d3ae519ec05869d4e722dfb9142f391cdb7ab
parent 238166 41a0c9bc40dfc529ee8f474e98f6ec61495d79f1
child 238168 904b5c1601f5d555383960a57676895c92598046
push id58133
push userryanvm@gmail.com
push dateWed, 08 Apr 2015 16:59:36 +0000
treeherdermozilla-inbound@8911c111a6ad [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmargaret
bugs1144385
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1144385 - Refactor buttons into DoorHanger. r=margaret
mobile/android/base/DoorHangerPopup.java
mobile/android/base/toolbar/SiteIdentityPopup.java
mobile/android/base/widget/DefaultDoorHanger.java
mobile/android/base/widget/DoorHanger.java
mobile/android/base/widget/DoorhangerConfig.java
mobile/android/base/widget/LoginDoorHanger.java
--- a/mobile/android/base/DoorHangerPopup.java
+++ b/mobile/android/base/DoorHangerPopup.java
@@ -1,32 +1,28 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import java.util.HashSet;
-import java.util.List;
 
-import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.prompts.PromptInput;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.widget.AnchoredPopup;
 import org.mozilla.gecko.widget.DoorHanger;
 
 import android.content.Context;
 import android.util.Log;
 import android.view.View;
-import android.widget.CheckBox;
 import org.mozilla.gecko.widget.DoorhangerConfig;
 
 public class DoorHangerPopup extends AnchoredPopup
                              implements GeckoEventListener,
                                         Tabs.OnTabsChangedListener,
                                         DoorHanger.OnButtonClickListener {
     private static final String LOGTAG = "GeckoDoorHangerPopup";
 
@@ -104,25 +100,26 @@ public class DoorHangerPopup extends Anc
         } catch (Exception e) {
             Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
         }
     }
 
     private DoorhangerConfig makeConfigFromJSON(JSONObject json) throws JSONException {
         final int tabId = json.getInt("tabID");
         final String id = json.getString("value");
-        final DoorhangerConfig config = new DoorhangerConfig(tabId, id);
+
+        final String typeString = json.optString("category");
+        final boolean isLogin = DoorHanger.Type.LOGIN.toString().equals(typeString);
+        final DoorHanger.Type doorhangerType = isLogin ? DoorHanger.Type.LOGIN : DoorHanger.Type.DEFAULT;
+
+        final DoorhangerConfig config = new DoorhangerConfig(tabId, id, doorhangerType, this);
 
         config.setMessage(json.getString("message"));
-        config.setButtons(json.getJSONArray("buttons"));
+        config.appendButtonsFromJSON(json.getJSONArray("buttons"));
         config.setOptions(json.getJSONObject("options"));
-        final String typeString = json.optString("category");
-        if (DoorHanger.Type.LOGIN.toString().equals(typeString)) {
-            config.setType(DoorHanger.Type.LOGIN);
-        }
 
         return config;
     }
 
     // This callback is automatically executed on the UI thread.
     @Override
     public void onTabChanged(final Tab tab, final Tabs.TabEvents msg, final Object data) {
         switch(msg) {
@@ -176,67 +173,33 @@ public class DoorHangerPopup extends Anc
         }
 
         if (!mInflated) {
             init();
         }
 
         final DoorHanger newDoorHanger = DoorHanger.Get(mContext, config);
 
-        final JSONArray buttons = config.getButtons();
-        for (int i = 0; i < buttons.length(); i++) {
-            try {
-                JSONObject buttonObject = buttons.getJSONObject(i);
-                String label = buttonObject.getString("label");
-                String tag = String.valueOf(buttonObject.getInt("callback"));
-                newDoorHanger.addButton(label, tag, this);
-            } catch (JSONException e) {
-                Log.e(LOGTAG, "Error creating doorhanger button", e);
-            }
-        }
-
         mDoorHangers.add(newDoorHanger);
         mContent.addView(newDoorHanger);
 
         // Only update the popup if we're adding a notification to the selected tab
         if (tabId == Tabs.getInstance().getSelectedTab().getId())
             updatePopup();
     }
 
 
     /*
      * DoorHanger.OnButtonClickListener implementation
      */
     @Override
-    public void onButtonClick(DoorHanger dh, String tag) {
-        JSONObject response = new JSONObject();
-        try {
-            response.put("callback", tag);
-
-            CheckBox checkBox = dh.getCheckBox();
-            // If the checkbox is being used, pass its value
-            if (checkBox != null) {
-                response.put("checked", checkBox.isChecked());
-            }
-
-            List<PromptInput> doorHangerInputs = dh.getInputs();
-            if (doorHangerInputs != null) {
-                JSONObject inputs = new JSONObject();
-                for (PromptInput input : doorHangerInputs) {
-                    inputs.put(input.getId(), input.getValue());
-                }
-                response.put("inputs", inputs);
-            }
-        } catch (JSONException e) {
-            Log.e(LOGTAG, "Error creating onClick response", e);
-        }
-
+    public void onButtonClick(JSONObject response, DoorHanger doorhanger) {
         GeckoEvent e = GeckoEvent.createBroadcastEvent("Doorhanger:Reply", response.toString());
         GeckoAppShell.sendEventToGecko(e);
-        removeDoorHanger(dh);
+        removeDoorHanger(doorhanger);
         updatePopup();
     }
 
     /**
      * Gets a doorhanger.
      *
      * This method must be called on the UI thread.
      */
--- a/mobile/android/base/toolbar/SiteIdentityPopup.java
+++ b/mobile/android/base/toolbar/SiteIdentityPopup.java
@@ -12,33 +12,34 @@ import org.mozilla.gecko.SiteIdentity;
 import org.mozilla.gecko.SiteIdentity.SecurityMode;
 import org.mozilla.gecko.SiteIdentity.MixedMode;
 import org.mozilla.gecko.SiteIdentity.TrackingMode;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.widget.AnchoredPopup;
 import org.mozilla.gecko.widget.DoorHanger;
 import org.mozilla.gecko.widget.DoorHanger.OnButtonClickListener;
-import org.json.JSONException;
 import org.json.JSONObject;
 
 import android.content.Context;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 import org.mozilla.gecko.widget.DoorhangerConfig;
 
 /**
  * SiteIdentityPopup is a singleton class that displays site identity data in
  * an arrow panel popup hanging from the lock icon in the browser toolbar.
  */
 public class SiteIdentityPopup extends AnchoredPopup {
+    public static enum ButtonType { DISABLE, ENABLE, KEEP_BLOCKING };
+
     private static final String LOGTAG = "GeckoSiteIdentityPopup";
 
     private static final String MIXED_CONTENT_SUPPORT_URL =
         "https://support.mozilla.org/kb/how-does-insecure-content-affect-safety-android";
 
     private static final String TRACKING_CONTENT_SUPPORT_URL =
         "https://support.mozilla.org/kb/firefox-android-tracking-protection";
 
@@ -137,88 +138,87 @@ public class SiteIdentityPopup extends A
         final String encrypted = siteIdentity.getEncrypted();
         mVerifier.setText(verifier + "\n" + encrypted);
     }
 
     private void addMixedContentNotification(boolean blocked) {
         // Remove any existing mixed content notification.
         removeMixedContentNotification();
 
-        final DoorhangerConfig config = new DoorhangerConfig();
+        final DoorhangerConfig config = new DoorhangerConfig(DoorHanger.Type.MIXED_CONTENT, mButtonClickListener);
         int icon;
         if (blocked) {
             icon = R.drawable.shield_enabled_doorhanger;
             config.setMessage(mContext.getString(R.string.blocked_mixed_content_message_top) + "\n\n" +
                       mContext.getString(R.string.blocked_mixed_content_message_bottom));
         } else {
             icon = R.drawable.shield_disabled_doorhanger;
             config.setMessage(mContext.getString(R.string.loaded_mixed_content_message));
         }
 
         config.setLink(mContext.getString(R.string.learn_more), MIXED_CONTENT_SUPPORT_URL, "\n\n");
-        config.setType(DoorHanger.Type.SITE);
+        addNotificationButtons(config, blocked);
+
         mMixedContentNotification = DoorHanger.Get(mContext, config);
         mMixedContentNotification.setIcon(icon);
 
-        addNotificationButtons(mMixedContentNotification, blocked);
 
         mContent.addView(mMixedContentNotification);
         mDivider.setVisibility(View.VISIBLE);
     }
 
     private void removeMixedContentNotification() {
         if (mMixedContentNotification != null) {
             mContent.removeView(mMixedContentNotification);
             mMixedContentNotification = null;
         }
     }
 
     private void addTrackingContentNotification(boolean blocked) {
         // Remove any existing tracking content notification.
         removeTrackingContentNotification();
 
-        final DoorhangerConfig config = new DoorhangerConfig();
+        final DoorhangerConfig config = new DoorhangerConfig(DoorHanger.Type.TRACKING, mButtonClickListener);
 
         int icon;
         if (blocked) {
             icon = R.drawable.shield_enabled_doorhanger;
             config.setMessage(mContext.getString(R.string.blocked_tracking_content_message_top) + "\n\n" +
                       mContext.getString(R.string.blocked_tracking_content_message_bottom));
         } else {
             icon = R.drawable.shield_disabled_doorhanger;
             config.setMessage(mContext.getString(R.string.loaded_tracking_content_message_top) + "\n\n" +
                       mContext.getString(R.string.loaded_tracking_content_message_bottom));
         }
 
         config.setLink(mContext.getString(R.string.learn_more), TRACKING_CONTENT_SUPPORT_URL, "\n\n");
-        config.setType(DoorHanger.Type.SITE);
+        addNotificationButtons(config, blocked);
+
         mTrackingContentNotification = DoorHanger.Get(mContext, config);
 
         mTrackingContentNotification.setIcon(icon);
 
-        addNotificationButtons(mTrackingContentNotification, blocked);
 
         mContent.addView(mTrackingContentNotification);
         mDivider.setVisibility(View.VISIBLE);
     }
 
     private void removeTrackingContentNotification() {
         if (mTrackingContentNotification != null) {
             mContent.removeView(mTrackingContentNotification);
             mTrackingContentNotification = null;
         }
     }
 
-    private void addNotificationButtons(DoorHanger dh, boolean blocked) {
-        // TODO: Add support for buttons in DoorHangerConfig.
+    private void addNotificationButtons(DoorhangerConfig config, boolean blocked) {
         if (blocked) {
-            dh.addButton(mContext.getString(R.string.disable_protection), "disable", mButtonClickListener);
-            dh.addButton(mContext.getString(R.string.keep_blocking), "keepBlocking", mButtonClickListener);
+            config.appendButton(mContext.getString(R.string.disable_protection), ButtonType.DISABLE.ordinal());
+            config.appendButton(mContext.getString(R.string.keep_blocking), ButtonType.KEEP_BLOCKING.ordinal());
         } else {
-            dh.addButton(mContext.getString(R.string.enable_protection), "enable", mButtonClickListener);
+            config.appendButton(mContext.getString(R.string.enable_protection), ButtonType.ENABLE.ordinal());
         }
     }
 
     /*
      * @param identityData A JSONObject that holds the current tab's identity data.
      */
     void setSiteIdentity(SiteIdentity siteIdentity) {
         mSiteIdentity = siteIdentity;
@@ -285,24 +285,15 @@ public class SiteIdentityPopup extends A
         super.dismiss();
         removeMixedContentNotification();
         removeTrackingContentNotification();
         mDivider.setVisibility(View.GONE);
     }
 
     private class PopupButtonListener implements OnButtonClickListener {
         @Override
-        public void onButtonClick(DoorHanger dh, String tag) {
-            try {
-                JSONObject data = new JSONObject();
-                data.put("allowContent", tag.equals("disable"));
-                data.put("contentType", (dh == mMixedContentNotification ? "mixed" : "tracking"));
-
-                GeckoEvent e = GeckoEvent.createBroadcastEvent("Session:Reload", data.toString());
-                GeckoAppShell.sendEventToGecko(e);
-            } catch (JSONException e) {
-                Log.e(LOGTAG, "Exception creating message to enable/disable content blocking", e);
-            }
-
+        public void onButtonClick(JSONObject response, DoorHanger doorhanger) {
+            GeckoEvent e = GeckoEvent.createBroadcastEvent("Session:Reload", response.toString());
+            GeckoAppShell.sendEventToGecko(e);
             dismiss();
         }
     }
 }
--- a/mobile/android/base/widget/DefaultDoorHanger.java
+++ b/mobile/android/base/widget/DefaultDoorHanger.java
@@ -1,45 +1,45 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.widget;
 
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.widget.Button;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.prompts.PromptInput;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.text.TextUtils;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.CheckBox;
+import org.mozilla.gecko.toolbar.SiteIdentityPopup;
 
 import java.util.ArrayList;
 import java.util.List;
 
 public class DefaultDoorHanger extends DoorHanger {
     private static final String LOGTAG = "GeckoDefaultDoorHanger";
 
     private final Resources mResources;
     private static int sSpinnerTextColor = -1;
 
     private List<PromptInput> mInputs;
     private CheckBox mCheckBox;
 
-    public DefaultDoorHanger(Context context, DoorhangerConfig config) {
-        this(context, config, Type.DEFAULT);
-    }
-
     public DefaultDoorHanger(Context context, DoorhangerConfig config, Type type) {
         super(context, config, type);
 
         mResources = getResources();
 
         if (sSpinnerTextColor == -1) {
             sSpinnerTextColor = getResources().getColor(R.color.text_color_primary_disable_only);
         }
@@ -57,25 +57,25 @@ public class DefaultDoorHanger extends D
         if (options != null) {
             setOptions(options);
         }
 
         final DoorhangerConfig.Link link = config.getLink();
         if (link != null) {
             addLink(link.label, link.url, link.delimiter);
         }
+
+        setButtons(config);
     }
 
-    @Override
-    public List<PromptInput> getInputs() {
+    private List<PromptInput> getInputs() {
         return mInputs;
     }
 
-    @Override
-    public CheckBox getCheckBox() {
+    private CheckBox getCheckBox() {
         return mCheckBox;
     }
 
     @Override
     public void setOptions(final JSONObject options) {
         super.setOptions(options);
         final JSONObject link = options.optJSONObject("link");
         if (link != null) {
@@ -110,16 +110,64 @@ public class DefaultDoorHanger extends D
         final String checkBoxText = options.optString("checkbox");
         if (!TextUtils.isEmpty(checkBoxText)) {
             mCheckBox = (CheckBox) findViewById(R.id.doorhanger_checkbox);
             mCheckBox.setText(checkBoxText);
             mCheckBox.setVisibility(VISIBLE);
         }
     }
 
+    @Override
+    protected Button createButtonInstance(final String text, final int id) {
+        final Button button = (Button) LayoutInflater.from(getContext()).inflate(R.layout.doorhanger_button, null);
+        button.setText(text);
+
+        button.setOnClickListener(new Button.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                final JSONObject response = new JSONObject();
+                try {
+                    // TODO: Bug 1149359 - Split this into each Doorhanger Type class.
+                    switch (mType) {
+                        case MIXED_CONTENT:
+                            response.put("allowContent", (id == SiteIdentityPopup.ButtonType.DISABLE.ordinal()));
+                            response.put("contentType", ("mixed"));
+                            break;
+                        case TRACKING:
+                            response.put("allowContent", (id == SiteIdentityPopup.ButtonType.DISABLE.ordinal()));
+                            response.put("contentType", ("tracking"));
+                            break;
+                        default:
+                            response.put("callback", id);
+
+                            CheckBox checkBox = getCheckBox();
+                            // If the checkbox is being used, pass its value
+                            if (checkBox != null) {
+                                response.put("checked", checkBox.isChecked());
+                            }
+
+                            List<PromptInput> doorHangerInputs = getInputs();
+                            if (doorHangerInputs != null) {
+                                JSONObject inputs = new JSONObject();
+                                for (PromptInput input : doorHangerInputs) {
+                                    inputs.put(input.getId(), input.getValue());
+                                }
+                                response.put("inputs", inputs);
+                            }
+                    }
+                    mOnButtonClickListener.onButtonClick(response, DefaultDoorHanger.this);
+                } catch (JSONException e) {
+                    Log.e(LOGTAG, "Error creating onClick response", e);
+                }
+            }
+        });
+
+        return button;
+    }
+
     private void styleInput(PromptInput input, View view) {
         if (input instanceof PromptInput.MenulistInput) {
             styleDropdownInputs(input, view);
         }
         view.setPadding(0, 0, 0, mResources.getDimensionPixelSize(R.dimen.doorhanger_padding));
     }
 
     private void styleDropdownInputs(PromptInput input, View view) {
--- a/mobile/android/base/widget/DoorHanger.java
+++ b/mobile/android/base/widget/DoorHanger.java
@@ -7,70 +7,70 @@ package org.mozilla.gecko.widget;
 
 import android.content.Context;
 import android.text.Html;
 import android.text.SpannableString;
 import android.text.Spanned;
 import android.text.method.LinkMovementMethod;
 import android.text.style.ForegroundColorSpan;
 import android.text.style.URLSpan;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.Button;
-import android.widget.CheckBox;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
+import org.json.JSONArray;
+import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.prompts.PromptInput;
-
-import java.util.List;
 
 public abstract class DoorHanger extends LinearLayout {
 
     public static DoorHanger Get(Context context, DoorhangerConfig config) {
         final Type type = config.getType();
-        if (type != null) {
-            switch (type) {
-                case LOGIN:
-                    return new LoginDoorHanger(context, config);
-                case SITE:
-                    return new DefaultDoorHanger(context, config, type);
-            }
+        switch (type) {
+            case LOGIN:
+                return new LoginDoorHanger(context, config);
+            case TRACKING:
+            case MIXED_CONTENT:
+                return new DefaultDoorHanger(context, config, type);
         }
-
-        return new DefaultDoorHanger(context, config);
+        return new DefaultDoorHanger(context, config, type);
     }
 
-    public static enum Type { DEFAULT, LOGIN, SITE }
+    public static enum Type { DEFAULT, LOGIN, TRACKING, MIXED_CONTENT}
 
     public interface OnButtonClickListener {
-        public void onButtonClick(DoorHanger dh, String tag);
+        public void onButtonClick(JSONObject response, DoorHanger doorhanger);
     }
 
-    private static final LayoutParams sButtonParams;
+    protected static final LayoutParams sButtonParams;
     static {
         sButtonParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 1.0f);
     }
 
     private static final String LOGTAG = "GeckoDoorHanger";
 
     // Divider between doorhangers.
     private final View mDivider;
 
-    private final LinearLayout mButtonsContainer;
+    protected final LinearLayout mButtonsContainer;
+    protected final OnButtonClickListener mOnButtonClickListener;
 
     // The tab this doorhanger is associated with.
     private final int mTabId;
 
     // DoorHanger identifier.
     private final String mIdentifier;
 
+    protected final Type mType;
+
     private final ImageView mIcon;
     private final TextView mMessage;
 
     protected Context mContext;
 
     protected int mDividerColor;
 
     protected boolean mPersistWhileVisible;
@@ -91,39 +91,60 @@ public abstract class DoorHanger extends
             default:
                 resource = R.layout.doorhanger;
         }
 
         LayoutInflater.from(context).inflate(resource, this);
         mDivider = findViewById(R.id.divider_doorhanger);
         mIcon = (ImageView) findViewById(R.id.doorhanger_icon);
         mMessage = (TextView) findViewById(R.id.doorhanger_message);
-        if (type == Type.SITE) {
+
+        // TODO: Bug 1149359 - split this into DoorHanger subclasses.
+        if (type == Type.TRACKING || type == Type.MIXED_CONTENT) {
             mMessage.setTextAppearance(getContext(), R.style.TextAppearance_DoorHanger_Small);
         }
+
+        mType = type;
+
         mButtonsContainer = (LinearLayout) findViewById(R.id.doorhanger_buttons);
+        mOnButtonClickListener = config.getButtonClickListener();
 
         mDividerColor = getResources().getColor(R.color.divider_light);
         setOrientation(VERTICAL);
     }
 
-    abstract protected void loadConfig(DoorhangerConfig config);
+    protected abstract void loadConfig(DoorhangerConfig config);
 
     protected void setOptions(final JSONObject options) {
         final int persistence = options.optInt("persistence");
         if (persistence > 0) {
             mPersistenceCount = persistence;
         }
 
         mPersistWhileVisible = options.optBoolean("persistWhileVisible");
 
         final long timeout = options.optLong("timeout");
         if (timeout > 0) {
             mTimeout = timeout;
         }
+    }
+
+    protected void setButtons(DoorhangerConfig config) {
+        final JSONArray buttons = config.getButtons();
+        final OnButtonClickListener listener = config.getButtonClickListener();
+        for (int i = 0; i < buttons.length(); i++) {
+            try {
+                final JSONObject buttonObject = buttons.getJSONObject(i);
+                final String label = buttonObject.getString("label");
+                final int callbackId = buttonObject.getInt("callback");
+                addButtonToLayout(label, callbackId);
+            } catch (JSONException e) {
+                Log.e(LOGTAG, "Error creating doorhanger button", e);
+            }
+        }
    }
 
     public int getTabId() {
         return mTabId;
     }
 
     public String getIdentifier() {
         return mIdentifier;
@@ -161,28 +182,23 @@ public abstract class DoorHanger extends
         ForegroundColorSpan colorSpan = new ForegroundColorSpan(mMessage.getCurrentTextColor());
         titleWithLink.setSpan(colorSpan, 0, title.length(), 0);
 
         titleWithLink.setSpan(linkSpan, title.length() + 1, titleWithLink.length(), 0);
         mMessage.setText(titleWithLink);
         mMessage.setMovementMethod(LinkMovementMethod.getInstance());
     }
 
-    public void addButton(final String text, final String tag, final OnButtonClickListener listener) {
-        final Button button = (Button) LayoutInflater.from(getContext()).inflate(R.layout.doorhanger_button, null);
-        button.setText(text);
-        button.setTag(tag);
-
-        button.setOnClickListener(new Button.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                listener.onButtonClick(DoorHanger.this, tag);
-            }
-        });
-
+    /**
+     * Creates and adds a button into the DoorHanger.
+     * @param text Button text
+     * @param id Identifier associated with the button
+     */
+    private void addButtonToLayout(String text, int id) {
+        final Button button = createButtonInstance(text, id);
         if (mButtonsContainer.getChildCount() == 0) {
             // If this is the first button we're adding, make the choices layout visible.
             mButtonsContainer.setVisibility(View.VISIBLE);
             // Make the divider above the buttons visible.
             View divider = findViewById(R.id.divider_buttons);
             divider.setVisibility(View.VISIBLE);
         } else {
             // Add a vertical divider between additional buttons.
@@ -190,16 +206,18 @@ public abstract class DoorHanger extends
             divider.setOrientation(Divider.Orientation.VERTICAL);
             divider.setBackgroundColor(mDividerColor);
             mButtonsContainer.addView(divider);
         }
 
         mButtonsContainer.addView(button, sButtonParams);
     }
 
+    protected abstract Button createButtonInstance(String text, int id);
+
     /*
      * Checks with persistence and timeout options to see if it's okay to remove a doorhanger.
      *
      * @param isShowing Whether or not this doorhanger is currently visible to the user.
      *                 (e.g. the DoorHanger view might be VISIBLE, but its parent could be hidden)
      */
     public boolean shouldRemove(boolean isShowing) {
         if (mPersistWhileVisible && isShowing) {
@@ -217,19 +235,9 @@ public abstract class DoorHanger extends
         }
 
         if (System.currentTimeMillis() <= mTimeout) {
             return false;
         }
 
         return true;
     }
-
-    // TODO: remove and expose through instance Button Handler.
-    public List<PromptInput> getInputs() {
-        return null;
-    }
-
-    public CheckBox getCheckBox() {
-        return null;
-    }
-
 }
--- a/mobile/android/base/widget/DoorhangerConfig.java
+++ b/mobile/android/base/widget/DoorhangerConfig.java
@@ -1,16 +1,18 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.widget;
 
+import android.util.Log;
 import org.json.JSONArray;
+import org.json.JSONException;
 import org.json.JSONObject;
 
 import org.mozilla.gecko.widget.DoorHanger.Type;
 
 public class DoorhangerConfig {
 
     public static class Link {
         public final String label;
@@ -19,47 +21,49 @@ public class DoorhangerConfig {
 
         private Link(String label, String url, String delimiter) {
             this.label = label;
             this.url = url;
             this.delimiter = delimiter;
         }
     }
 
+    private static final String LOGTAG = "DoorhangerConfig";
+
     private final int tabId;
     private final String id;
-    private DoorHanger.Type type;
+    private final DoorHanger.OnButtonClickListener buttonClickListener;
+    private final DoorHanger.Type type;
     private String message;
     private JSONObject options;
     private Link link;
-    private JSONArray buttons;
+    private JSONArray buttons = new JSONArray();
 
-    public DoorhangerConfig() {
+    public DoorhangerConfig(Type type, DoorHanger.OnButtonClickListener listener) {
         // XXX: This should only be used by SiteIdentityPopup doorhangers which
         // don't need tab or id references, until bug 1141904 unifies doorhangers.
-        this(-1, null);
+
+        this(-1, null, type, listener);
     }
 
-    public DoorhangerConfig(int tabId, String id) {
+    public DoorhangerConfig(int tabId, String id, DoorHanger.Type type, DoorHanger.OnButtonClickListener buttonClickListener) {
         this.tabId = tabId;
         this.id = id;
+        this.type = type;
+        this.buttonClickListener = buttonClickListener;
     }
 
     public int getTabId() {
         return tabId;
     }
 
     public String getId() {
         return id;
     }
 
-    public void setType(Type type) {
-        this.type = type;
-    }
-
     public Type getType() {
         return type;
     }
 
     public void setMessage(String message) {
         this.message = message;
     }
 
@@ -70,18 +74,43 @@ public class DoorhangerConfig {
     public void setOptions(JSONObject options) {
         this.options = options;
     }
 
     public JSONObject getOptions() {
         return options;
     }
 
-    public void setButtons(JSONArray buttons) {
-        this.buttons = buttons;
+    /**
+     * Add buttons from JSON to the Config object.
+     * @param buttons JSONArray of JSONObjects of the form { label: <label>, callback: <callback_id> }
+     */
+    public void appendButtonsFromJSON(JSONArray buttons) {
+        try {
+            for (int i = 0; i < buttons.length(); i++) {
+                this.buttons.put(buttons.get(i));
+            }
+        } catch (JSONException e) {
+            Log.e(LOGTAG, "Error parsing buttons from JSON", e);
+        }
+    }
+
+    public void appendButton(String label, int callbackId) {
+        final JSONObject button = new JSONObject();
+        try {
+            button.put("label", label);
+            button.put("callback", callbackId);
+            this.buttons.put(button);
+        } catch (JSONException e) {
+            Log.e(LOGTAG, "Error creating button", e);
+        }
+    }
+
+    public DoorHanger.OnButtonClickListener getButtonClickListener() {
+        return this.buttonClickListener;
     }
 
     public JSONArray getButtons() {
         return buttons;
     }
 
     public void setLink(String label, String url, String delimiter) {
         this.link = new Link(label, url, delimiter);
--- a/mobile/android/base/widget/LoginDoorHanger.java
+++ b/mobile/android/base/widget/LoginDoorHanger.java
@@ -4,17 +4,19 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.widget;
 
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.drawable.BitmapDrawable;
 import android.util.Log;
+import android.view.LayoutInflater;
 import android.view.View;
+import android.widget.Button;
 import android.widget.TextView;
 import ch.boye.httpclientandroidlib.util.TextUtils;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 
@@ -32,17 +34,17 @@ public class LoginDoorHanger extends Doo
 
         loadConfig(config);
     }
 
     @Override
     protected void loadConfig(DoorhangerConfig config) {
         setOptions(config.getOptions());
         setMessage(config.getMessage());
-
+        setButtons(config);
     }
 
     @Override
     protected void setOptions(final JSONObject options) {
         super.setOptions(options);
 
         final JSONObject titleObj = options.optJSONObject("title");
         if (titleObj != null) {
@@ -71,9 +73,30 @@ public class LoginDoorHanger extends Doo
         final String subtext = options.optString("subtext");
         if (!TextUtils.isEmpty(subtext)) {
             mLogin.setText(subtext);
             mLogin.setVisibility(View.VISIBLE);
         } else {
             mLogin.setVisibility(View.GONE);
         }
     }
+
+    @Override
+    protected Button createButtonInstance(final String text, final int id) {
+        final Button button = (Button) LayoutInflater.from(getContext()).inflate(R.layout.doorhanger_button, null);
+        button.setText(text);
+
+        button.setOnClickListener(new Button.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                final JSONObject response = new JSONObject();
+                try {
+                    response.put("callback", id);
+                } catch (JSONException e) {
+                    Log.e(LOGTAG, "Error making doorhanger response message");
+                }
+                mOnButtonClickListener.onButtonClick(response, LoginDoorHanger.this);
+            }
+        });
+
+        return button;
+    }
 }