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; michael@0: michael@0: import org.mozilla.gecko.util.GeckoEventListener; michael@0: import org.mozilla.gecko.widget.ArrowPopup; michael@0: import org.mozilla.gecko.widget.DoorHanger; 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.os.Build; michael@0: import android.util.Log; michael@0: import android.view.View; michael@0: import android.widget.CheckBox; michael@0: michael@0: import java.util.HashSet; michael@0: import java.util.List; michael@0: michael@0: public class DoorHangerPopup extends ArrowPopup michael@0: implements GeckoEventListener, michael@0: Tabs.OnTabsChangedListener, michael@0: DoorHanger.OnButtonClickListener { michael@0: private static final String LOGTAG = "GeckoDoorHangerPopup"; michael@0: michael@0: // Stores a set of all active DoorHanger notifications. A DoorHanger is michael@0: // uniquely identified by its tabId and value. michael@0: private HashSet mDoorHangers; michael@0: michael@0: // Whether or not the doorhanger popup is disabled. michael@0: private boolean mDisabled; michael@0: michael@0: DoorHangerPopup(GeckoApp activity) { michael@0: super(activity); michael@0: michael@0: mDoorHangers = new HashSet(); michael@0: michael@0: registerEventListener("Doorhanger:Add"); michael@0: registerEventListener("Doorhanger:Remove"); michael@0: Tabs.registerOnTabsChangedListener(this); michael@0: } michael@0: michael@0: void destroy() { michael@0: unregisterEventListener("Doorhanger:Add"); michael@0: unregisterEventListener("Doorhanger:Remove"); michael@0: Tabs.unregisterOnTabsChangedListener(this); michael@0: } michael@0: michael@0: /** michael@0: * Temporarily disables the doorhanger popup. If the popup is disabled, michael@0: * it will not be shown to the user, but it will continue to process michael@0: * calls to add/remove doorhanger notifications. michael@0: */ michael@0: void disable() { michael@0: mDisabled = true; michael@0: updatePopup(); michael@0: } michael@0: michael@0: /** michael@0: * Re-enables the doorhanger popup. michael@0: */ michael@0: void enable() { michael@0: mDisabled = false; michael@0: updatePopup(); michael@0: } michael@0: michael@0: @Override michael@0: public void handleMessage(String event, JSONObject geckoObject) { michael@0: try { michael@0: if (event.equals("Doorhanger:Add")) { michael@0: final int tabId = geckoObject.getInt("tabID"); michael@0: final String value = geckoObject.getString("value"); michael@0: final String message = geckoObject.getString("message"); michael@0: final JSONArray buttons = geckoObject.getJSONArray("buttons"); michael@0: final JSONObject options = geckoObject.getJSONObject("options"); michael@0: michael@0: mActivity.runOnUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: addDoorHanger(tabId, value, message, buttons, options); michael@0: } michael@0: }); michael@0: } else if (event.equals("Doorhanger:Remove")) { michael@0: final int tabId = geckoObject.getInt("tabID"); michael@0: final String value = geckoObject.getString("value"); michael@0: michael@0: mActivity.runOnUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: DoorHanger doorHanger = getDoorHanger(tabId, value); michael@0: if (doorHanger == null) michael@0: return; michael@0: michael@0: removeDoorHanger(doorHanger); michael@0: updatePopup(); michael@0: } michael@0: }); michael@0: } michael@0: } catch (Exception e) { michael@0: Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); michael@0: } michael@0: } michael@0: michael@0: // This callback is automatically executed on the UI thread. michael@0: @Override michael@0: public void onTabChanged(final Tab tab, final Tabs.TabEvents msg, final Object data) { michael@0: switch(msg) { michael@0: case CLOSED: michael@0: // Remove any doorhangers for a tab when it's closed (make michael@0: // a temporary set to avoid a ConcurrentModificationException) michael@0: HashSet doorHangersToRemove = new HashSet(); michael@0: for (DoorHanger dh : mDoorHangers) { michael@0: if (dh.getTabId() == tab.getId()) michael@0: doorHangersToRemove.add(dh); michael@0: } michael@0: for (DoorHanger dh : doorHangersToRemove) { michael@0: removeDoorHanger(dh); michael@0: } michael@0: break; michael@0: michael@0: case LOCATION_CHANGE: michael@0: // Only remove doorhangers if the popup is hidden or if we're navigating to a new URL michael@0: if (!isShowing() || !data.equals(tab.getURL())) michael@0: removeTransientDoorHangers(tab.getId()); michael@0: michael@0: // Update the popup if the location change was on the current tab michael@0: if (Tabs.getInstance().isSelectedTab(tab)) michael@0: updatePopup(); michael@0: break; michael@0: michael@0: case SELECTED: michael@0: // Always update the popup when a new tab is selected. This will cover cases michael@0: // where a different tab was closed, since we always need to select a new tab. michael@0: updatePopup(); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Adds a doorhanger. michael@0: * michael@0: * This method must be called on the UI thread. michael@0: */ michael@0: void addDoorHanger(final int tabId, final String value, final String message, michael@0: final JSONArray buttons, final JSONObject options) { michael@0: // Don't add a doorhanger for a tab that doesn't exist michael@0: if (Tabs.getInstance().getTab(tabId) == null) { michael@0: return; michael@0: } michael@0: michael@0: // Replace the doorhanger if it already exists michael@0: DoorHanger oldDoorHanger = getDoorHanger(tabId, value); michael@0: if (oldDoorHanger != null) { michael@0: removeDoorHanger(oldDoorHanger); michael@0: } michael@0: michael@0: if (!mInflated) { michael@0: init(); michael@0: } michael@0: michael@0: final DoorHanger newDoorHanger = new DoorHanger(mActivity, tabId, value); michael@0: newDoorHanger.setMessage(message); michael@0: newDoorHanger.setOptions(options); michael@0: michael@0: for (int i = 0; i < buttons.length(); i++) { michael@0: try { michael@0: JSONObject buttonObject = buttons.getJSONObject(i); michael@0: String label = buttonObject.getString("label"); michael@0: String tag = String.valueOf(buttonObject.getInt("callback")); michael@0: newDoorHanger.addButton(label, tag, this); michael@0: } catch (JSONException e) { michael@0: Log.e(LOGTAG, "Error creating doorhanger button", e); michael@0: } michael@0: } michael@0: michael@0: mDoorHangers.add(newDoorHanger); michael@0: mContent.addView(newDoorHanger); michael@0: michael@0: // Only update the popup if we're adding a notifcation to the selected tab michael@0: if (tabId == Tabs.getInstance().getSelectedTab().getId()) michael@0: updatePopup(); michael@0: } michael@0: michael@0: michael@0: /* michael@0: * DoorHanger.OnButtonClickListener implementation michael@0: */ michael@0: @Override michael@0: public void onButtonClick(DoorHanger dh, String tag) { michael@0: JSONObject response = new JSONObject(); michael@0: try { michael@0: response.put("callback", tag); michael@0: michael@0: CheckBox checkBox = dh.getCheckBox(); michael@0: // If the checkbox is being used, pass its value michael@0: if (checkBox != null) { michael@0: response.put("checked", checkBox.isChecked()); michael@0: } michael@0: michael@0: List doorHangerInputs = dh.getInputs(); michael@0: if (doorHangerInputs != null) { michael@0: JSONObject inputs = new JSONObject(); michael@0: for (PromptInput input : doorHangerInputs) { michael@0: inputs.put(input.getId(), input.getValue()); michael@0: } michael@0: response.put("inputs", inputs); michael@0: } michael@0: } catch (JSONException e) { michael@0: Log.e(LOGTAG, "Error creating onClick response", e); michael@0: } michael@0: michael@0: GeckoEvent e = GeckoEvent.createBroadcastEvent("Doorhanger:Reply", response.toString()); michael@0: GeckoAppShell.sendEventToGecko(e); michael@0: removeDoorHanger(dh); michael@0: updatePopup(); michael@0: } michael@0: michael@0: /** michael@0: * Gets a doorhanger. michael@0: * michael@0: * This method must be called on the UI thread. michael@0: */ michael@0: DoorHanger getDoorHanger(int tabId, String value) { michael@0: for (DoorHanger dh : mDoorHangers) { michael@0: if (dh.getTabId() == tabId && dh.getValue().equals(value)) michael@0: return dh; michael@0: } michael@0: michael@0: // If there's no doorhanger for the given tabId and value, return null michael@0: return null; michael@0: } michael@0: michael@0: /** michael@0: * Removes a doorhanger. michael@0: * michael@0: * This method must be called on the UI thread. michael@0: */ michael@0: void removeDoorHanger(final DoorHanger doorHanger) { michael@0: mDoorHangers.remove(doorHanger); michael@0: mContent.removeView(doorHanger); michael@0: } michael@0: michael@0: /** michael@0: * Removes doorhangers for a given tab. michael@0: * michael@0: * This method must be called on the UI thread. michael@0: */ michael@0: void removeTransientDoorHangers(int tabId) { michael@0: // Make a temporary set to avoid a ConcurrentModificationException michael@0: HashSet doorHangersToRemove = new HashSet(); michael@0: for (DoorHanger dh : mDoorHangers) { michael@0: // Only remove transient doorhangers for the given tab michael@0: if (dh.getTabId() == tabId && dh.shouldRemove(isShowing())) michael@0: doorHangersToRemove.add(dh); michael@0: } michael@0: michael@0: for (DoorHanger dh : doorHangersToRemove) { michael@0: removeDoorHanger(dh); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Updates the popup state. michael@0: * michael@0: * This method must be called on the UI thread. michael@0: */ michael@0: void updatePopup() { michael@0: // Bail if the selected tab is null, if there are no active doorhangers, michael@0: // if we haven't inflated the layout yet (this can happen if updatePopup() michael@0: // is called before the runnable from addDoorHanger() runs), or if the michael@0: // doorhanger popup is temporarily disabled. michael@0: Tab tab = Tabs.getInstance().getSelectedTab(); michael@0: if (tab == null || mDoorHangers.size() == 0 || !mInflated || mDisabled) { michael@0: dismiss(); michael@0: return; michael@0: } michael@0: michael@0: // Show doorhangers for the selected tab michael@0: int tabId = tab.getId(); michael@0: boolean shouldShowPopup = false; michael@0: for (DoorHanger dh : mDoorHangers) { michael@0: if (dh.getTabId() == tabId) { michael@0: dh.setVisibility(View.VISIBLE); michael@0: shouldShowPopup = true; michael@0: } else { michael@0: dh.setVisibility(View.GONE); michael@0: } michael@0: } michael@0: michael@0: // Dismiss the popup if there are no doorhangers to show for this tab michael@0: if (!shouldShowPopup) { michael@0: dismiss(); michael@0: return; michael@0: } michael@0: michael@0: showDividers(); michael@0: if (isShowing()) { michael@0: show(); michael@0: return; michael@0: } michael@0: michael@0: // Make the popup focusable for accessibility. This gets done here michael@0: // so the node can be accessibility focused, but on pre-ICS devices this michael@0: // causes crashes, so it is done after the popup is shown. michael@0: if (Build.VERSION.SDK_INT >= 14) { michael@0: setFocusable(true); michael@0: } michael@0: michael@0: show(); michael@0: michael@0: if (Build.VERSION.SDK_INT < 14) { michael@0: // Make the popup focusable for keyboard accessibility. michael@0: setFocusable(true); michael@0: } michael@0: } michael@0: michael@0: //Show all inter-DoorHanger dividers (ie. Dividers on all visible DoorHangers except the last one) michael@0: private void showDividers() { michael@0: int count = mContent.getChildCount(); michael@0: DoorHanger lastVisibleDoorHanger = null; michael@0: michael@0: for (int i = 0; i < count; i++) { michael@0: DoorHanger dh = (DoorHanger) mContent.getChildAt(i); michael@0: dh.showDivider(); michael@0: if (dh.getVisibility() == View.VISIBLE) { michael@0: lastVisibleDoorHanger = dh; michael@0: } michael@0: } michael@0: if (lastVisibleDoorHanger != null) { michael@0: lastVisibleDoorHanger.hideDivider(); michael@0: } michael@0: } michael@0: michael@0: private void registerEventListener(String event) { michael@0: GeckoAppShell.getEventDispatcher().registerEventListener(event, this); michael@0: } michael@0: michael@0: private void unregisterEventListener(String event) { michael@0: GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this); michael@0: } michael@0: michael@0: @Override michael@0: public void dismiss() { michael@0: // If the popup is focusable while it is hidden, we run into crashes michael@0: // on pre-ICS devices when the popup gets focus before it is shown. michael@0: setFocusable(false); michael@0: super.dismiss(); michael@0: } michael@0: }