Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 package org.mozilla.gecko;
8 import org.mozilla.gecko.util.GeckoEventListener;
9 import org.mozilla.gecko.widget.ArrowPopup;
10 import org.mozilla.gecko.widget.DoorHanger;
11 import org.mozilla.gecko.prompts.PromptInput;
13 import org.json.JSONArray;
14 import org.json.JSONException;
15 import org.json.JSONObject;
17 import android.os.Build;
18 import android.util.Log;
19 import android.view.View;
20 import android.widget.CheckBox;
22 import java.util.HashSet;
23 import java.util.List;
25 public class DoorHangerPopup extends ArrowPopup
26 implements GeckoEventListener,
27 Tabs.OnTabsChangedListener,
28 DoorHanger.OnButtonClickListener {
29 private static final String LOGTAG = "GeckoDoorHangerPopup";
31 // Stores a set of all active DoorHanger notifications. A DoorHanger is
32 // uniquely identified by its tabId and value.
33 private HashSet<DoorHanger> mDoorHangers;
35 // Whether or not the doorhanger popup is disabled.
36 private boolean mDisabled;
38 DoorHangerPopup(GeckoApp activity) {
39 super(activity);
41 mDoorHangers = new HashSet<DoorHanger>();
43 registerEventListener("Doorhanger:Add");
44 registerEventListener("Doorhanger:Remove");
45 Tabs.registerOnTabsChangedListener(this);
46 }
48 void destroy() {
49 unregisterEventListener("Doorhanger:Add");
50 unregisterEventListener("Doorhanger:Remove");
51 Tabs.unregisterOnTabsChangedListener(this);
52 }
54 /**
55 * Temporarily disables the doorhanger popup. If the popup is disabled,
56 * it will not be shown to the user, but it will continue to process
57 * calls to add/remove doorhanger notifications.
58 */
59 void disable() {
60 mDisabled = true;
61 updatePopup();
62 }
64 /**
65 * Re-enables the doorhanger popup.
66 */
67 void enable() {
68 mDisabled = false;
69 updatePopup();
70 }
72 @Override
73 public void handleMessage(String event, JSONObject geckoObject) {
74 try {
75 if (event.equals("Doorhanger:Add")) {
76 final int tabId = geckoObject.getInt("tabID");
77 final String value = geckoObject.getString("value");
78 final String message = geckoObject.getString("message");
79 final JSONArray buttons = geckoObject.getJSONArray("buttons");
80 final JSONObject options = geckoObject.getJSONObject("options");
82 mActivity.runOnUiThread(new Runnable() {
83 @Override
84 public void run() {
85 addDoorHanger(tabId, value, message, buttons, options);
86 }
87 });
88 } else if (event.equals("Doorhanger:Remove")) {
89 final int tabId = geckoObject.getInt("tabID");
90 final String value = geckoObject.getString("value");
92 mActivity.runOnUiThread(new Runnable() {
93 @Override
94 public void run() {
95 DoorHanger doorHanger = getDoorHanger(tabId, value);
96 if (doorHanger == null)
97 return;
99 removeDoorHanger(doorHanger);
100 updatePopup();
101 }
102 });
103 }
104 } catch (Exception e) {
105 Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
106 }
107 }
109 // This callback is automatically executed on the UI thread.
110 @Override
111 public void onTabChanged(final Tab tab, final Tabs.TabEvents msg, final Object data) {
112 switch(msg) {
113 case CLOSED:
114 // Remove any doorhangers for a tab when it's closed (make
115 // a temporary set to avoid a ConcurrentModificationException)
116 HashSet<DoorHanger> doorHangersToRemove = new HashSet<DoorHanger>();
117 for (DoorHanger dh : mDoorHangers) {
118 if (dh.getTabId() == tab.getId())
119 doorHangersToRemove.add(dh);
120 }
121 for (DoorHanger dh : doorHangersToRemove) {
122 removeDoorHanger(dh);
123 }
124 break;
126 case LOCATION_CHANGE:
127 // Only remove doorhangers if the popup is hidden or if we're navigating to a new URL
128 if (!isShowing() || !data.equals(tab.getURL()))
129 removeTransientDoorHangers(tab.getId());
131 // Update the popup if the location change was on the current tab
132 if (Tabs.getInstance().isSelectedTab(tab))
133 updatePopup();
134 break;
136 case SELECTED:
137 // Always update the popup when a new tab is selected. This will cover cases
138 // where a different tab was closed, since we always need to select a new tab.
139 updatePopup();
140 break;
141 }
142 }
144 /**
145 * Adds a doorhanger.
146 *
147 * This method must be called on the UI thread.
148 */
149 void addDoorHanger(final int tabId, final String value, final String message,
150 final JSONArray buttons, final JSONObject options) {
151 // Don't add a doorhanger for a tab that doesn't exist
152 if (Tabs.getInstance().getTab(tabId) == null) {
153 return;
154 }
156 // Replace the doorhanger if it already exists
157 DoorHanger oldDoorHanger = getDoorHanger(tabId, value);
158 if (oldDoorHanger != null) {
159 removeDoorHanger(oldDoorHanger);
160 }
162 if (!mInflated) {
163 init();
164 }
166 final DoorHanger newDoorHanger = new DoorHanger(mActivity, tabId, value);
167 newDoorHanger.setMessage(message);
168 newDoorHanger.setOptions(options);
170 for (int i = 0; i < buttons.length(); i++) {
171 try {
172 JSONObject buttonObject = buttons.getJSONObject(i);
173 String label = buttonObject.getString("label");
174 String tag = String.valueOf(buttonObject.getInt("callback"));
175 newDoorHanger.addButton(label, tag, this);
176 } catch (JSONException e) {
177 Log.e(LOGTAG, "Error creating doorhanger button", e);
178 }
179 }
181 mDoorHangers.add(newDoorHanger);
182 mContent.addView(newDoorHanger);
184 // Only update the popup if we're adding a notifcation to the selected tab
185 if (tabId == Tabs.getInstance().getSelectedTab().getId())
186 updatePopup();
187 }
190 /*
191 * DoorHanger.OnButtonClickListener implementation
192 */
193 @Override
194 public void onButtonClick(DoorHanger dh, String tag) {
195 JSONObject response = new JSONObject();
196 try {
197 response.put("callback", tag);
199 CheckBox checkBox = dh.getCheckBox();
200 // If the checkbox is being used, pass its value
201 if (checkBox != null) {
202 response.put("checked", checkBox.isChecked());
203 }
205 List<PromptInput> doorHangerInputs = dh.getInputs();
206 if (doorHangerInputs != null) {
207 JSONObject inputs = new JSONObject();
208 for (PromptInput input : doorHangerInputs) {
209 inputs.put(input.getId(), input.getValue());
210 }
211 response.put("inputs", inputs);
212 }
213 } catch (JSONException e) {
214 Log.e(LOGTAG, "Error creating onClick response", e);
215 }
217 GeckoEvent e = GeckoEvent.createBroadcastEvent("Doorhanger:Reply", response.toString());
218 GeckoAppShell.sendEventToGecko(e);
219 removeDoorHanger(dh);
220 updatePopup();
221 }
223 /**
224 * Gets a doorhanger.
225 *
226 * This method must be called on the UI thread.
227 */
228 DoorHanger getDoorHanger(int tabId, String value) {
229 for (DoorHanger dh : mDoorHangers) {
230 if (dh.getTabId() == tabId && dh.getValue().equals(value))
231 return dh;
232 }
234 // If there's no doorhanger for the given tabId and value, return null
235 return null;
236 }
238 /**
239 * Removes a doorhanger.
240 *
241 * This method must be called on the UI thread.
242 */
243 void removeDoorHanger(final DoorHanger doorHanger) {
244 mDoorHangers.remove(doorHanger);
245 mContent.removeView(doorHanger);
246 }
248 /**
249 * Removes doorhangers for a given tab.
250 *
251 * This method must be called on the UI thread.
252 */
253 void removeTransientDoorHangers(int tabId) {
254 // Make a temporary set to avoid a ConcurrentModificationException
255 HashSet<DoorHanger> doorHangersToRemove = new HashSet<DoorHanger>();
256 for (DoorHanger dh : mDoorHangers) {
257 // Only remove transient doorhangers for the given tab
258 if (dh.getTabId() == tabId && dh.shouldRemove(isShowing()))
259 doorHangersToRemove.add(dh);
260 }
262 for (DoorHanger dh : doorHangersToRemove) {
263 removeDoorHanger(dh);
264 }
265 }
267 /**
268 * Updates the popup state.
269 *
270 * This method must be called on the UI thread.
271 */
272 void updatePopup() {
273 // Bail if the selected tab is null, if there are no active doorhangers,
274 // if we haven't inflated the layout yet (this can happen if updatePopup()
275 // is called before the runnable from addDoorHanger() runs), or if the
276 // doorhanger popup is temporarily disabled.
277 Tab tab = Tabs.getInstance().getSelectedTab();
278 if (tab == null || mDoorHangers.size() == 0 || !mInflated || mDisabled) {
279 dismiss();
280 return;
281 }
283 // Show doorhangers for the selected tab
284 int tabId = tab.getId();
285 boolean shouldShowPopup = false;
286 for (DoorHanger dh : mDoorHangers) {
287 if (dh.getTabId() == tabId) {
288 dh.setVisibility(View.VISIBLE);
289 shouldShowPopup = true;
290 } else {
291 dh.setVisibility(View.GONE);
292 }
293 }
295 // Dismiss the popup if there are no doorhangers to show for this tab
296 if (!shouldShowPopup) {
297 dismiss();
298 return;
299 }
301 showDividers();
302 if (isShowing()) {
303 show();
304 return;
305 }
307 // Make the popup focusable for accessibility. This gets done here
308 // so the node can be accessibility focused, but on pre-ICS devices this
309 // causes crashes, so it is done after the popup is shown.
310 if (Build.VERSION.SDK_INT >= 14) {
311 setFocusable(true);
312 }
314 show();
316 if (Build.VERSION.SDK_INT < 14) {
317 // Make the popup focusable for keyboard accessibility.
318 setFocusable(true);
319 }
320 }
322 //Show all inter-DoorHanger dividers (ie. Dividers on all visible DoorHangers except the last one)
323 private void showDividers() {
324 int count = mContent.getChildCount();
325 DoorHanger lastVisibleDoorHanger = null;
327 for (int i = 0; i < count; i++) {
328 DoorHanger dh = (DoorHanger) mContent.getChildAt(i);
329 dh.showDivider();
330 if (dh.getVisibility() == View.VISIBLE) {
331 lastVisibleDoorHanger = dh;
332 }
333 }
334 if (lastVisibleDoorHanger != null) {
335 lastVisibleDoorHanger.hideDivider();
336 }
337 }
339 private void registerEventListener(String event) {
340 GeckoAppShell.getEventDispatcher().registerEventListener(event, this);
341 }
343 private void unregisterEventListener(String event) {
344 GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this);
345 }
347 @Override
348 public void dismiss() {
349 // If the popup is focusable while it is hidden, we run into crashes
350 // on pre-ICS devices when the popup gets focus before it is shown.
351 setFocusable(false);
352 super.dismiss();
353 }
354 }