1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/DoorHangerPopup.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,354 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko; 1.10 + 1.11 +import org.mozilla.gecko.util.GeckoEventListener; 1.12 +import org.mozilla.gecko.widget.ArrowPopup; 1.13 +import org.mozilla.gecko.widget.DoorHanger; 1.14 +import org.mozilla.gecko.prompts.PromptInput; 1.15 + 1.16 +import org.json.JSONArray; 1.17 +import org.json.JSONException; 1.18 +import org.json.JSONObject; 1.19 + 1.20 +import android.os.Build; 1.21 +import android.util.Log; 1.22 +import android.view.View; 1.23 +import android.widget.CheckBox; 1.24 + 1.25 +import java.util.HashSet; 1.26 +import java.util.List; 1.27 + 1.28 +public class DoorHangerPopup extends ArrowPopup 1.29 + implements GeckoEventListener, 1.30 + Tabs.OnTabsChangedListener, 1.31 + DoorHanger.OnButtonClickListener { 1.32 + private static final String LOGTAG = "GeckoDoorHangerPopup"; 1.33 + 1.34 + // Stores a set of all active DoorHanger notifications. A DoorHanger is 1.35 + // uniquely identified by its tabId and value. 1.36 + private HashSet<DoorHanger> mDoorHangers; 1.37 + 1.38 + // Whether or not the doorhanger popup is disabled. 1.39 + private boolean mDisabled; 1.40 + 1.41 + DoorHangerPopup(GeckoApp activity) { 1.42 + super(activity); 1.43 + 1.44 + mDoorHangers = new HashSet<DoorHanger>(); 1.45 + 1.46 + registerEventListener("Doorhanger:Add"); 1.47 + registerEventListener("Doorhanger:Remove"); 1.48 + Tabs.registerOnTabsChangedListener(this); 1.49 + } 1.50 + 1.51 + void destroy() { 1.52 + unregisterEventListener("Doorhanger:Add"); 1.53 + unregisterEventListener("Doorhanger:Remove"); 1.54 + Tabs.unregisterOnTabsChangedListener(this); 1.55 + } 1.56 + 1.57 + /** 1.58 + * Temporarily disables the doorhanger popup. If the popup is disabled, 1.59 + * it will not be shown to the user, but it will continue to process 1.60 + * calls to add/remove doorhanger notifications. 1.61 + */ 1.62 + void disable() { 1.63 + mDisabled = true; 1.64 + updatePopup(); 1.65 + } 1.66 + 1.67 + /** 1.68 + * Re-enables the doorhanger popup. 1.69 + */ 1.70 + void enable() { 1.71 + mDisabled = false; 1.72 + updatePopup(); 1.73 + } 1.74 + 1.75 + @Override 1.76 + public void handleMessage(String event, JSONObject geckoObject) { 1.77 + try { 1.78 + if (event.equals("Doorhanger:Add")) { 1.79 + final int tabId = geckoObject.getInt("tabID"); 1.80 + final String value = geckoObject.getString("value"); 1.81 + final String message = geckoObject.getString("message"); 1.82 + final JSONArray buttons = geckoObject.getJSONArray("buttons"); 1.83 + final JSONObject options = geckoObject.getJSONObject("options"); 1.84 + 1.85 + mActivity.runOnUiThread(new Runnable() { 1.86 + @Override 1.87 + public void run() { 1.88 + addDoorHanger(tabId, value, message, buttons, options); 1.89 + } 1.90 + }); 1.91 + } else if (event.equals("Doorhanger:Remove")) { 1.92 + final int tabId = geckoObject.getInt("tabID"); 1.93 + final String value = geckoObject.getString("value"); 1.94 + 1.95 + mActivity.runOnUiThread(new Runnable() { 1.96 + @Override 1.97 + public void run() { 1.98 + DoorHanger doorHanger = getDoorHanger(tabId, value); 1.99 + if (doorHanger == null) 1.100 + return; 1.101 + 1.102 + removeDoorHanger(doorHanger); 1.103 + updatePopup(); 1.104 + } 1.105 + }); 1.106 + } 1.107 + } catch (Exception e) { 1.108 + Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); 1.109 + } 1.110 + } 1.111 + 1.112 + // This callback is automatically executed on the UI thread. 1.113 + @Override 1.114 + public void onTabChanged(final Tab tab, final Tabs.TabEvents msg, final Object data) { 1.115 + switch(msg) { 1.116 + case CLOSED: 1.117 + // Remove any doorhangers for a tab when it's closed (make 1.118 + // a temporary set to avoid a ConcurrentModificationException) 1.119 + HashSet<DoorHanger> doorHangersToRemove = new HashSet<DoorHanger>(); 1.120 + for (DoorHanger dh : mDoorHangers) { 1.121 + if (dh.getTabId() == tab.getId()) 1.122 + doorHangersToRemove.add(dh); 1.123 + } 1.124 + for (DoorHanger dh : doorHangersToRemove) { 1.125 + removeDoorHanger(dh); 1.126 + } 1.127 + break; 1.128 + 1.129 + case LOCATION_CHANGE: 1.130 + // Only remove doorhangers if the popup is hidden or if we're navigating to a new URL 1.131 + if (!isShowing() || !data.equals(tab.getURL())) 1.132 + removeTransientDoorHangers(tab.getId()); 1.133 + 1.134 + // Update the popup if the location change was on the current tab 1.135 + if (Tabs.getInstance().isSelectedTab(tab)) 1.136 + updatePopup(); 1.137 + break; 1.138 + 1.139 + case SELECTED: 1.140 + // Always update the popup when a new tab is selected. This will cover cases 1.141 + // where a different tab was closed, since we always need to select a new tab. 1.142 + updatePopup(); 1.143 + break; 1.144 + } 1.145 + } 1.146 + 1.147 + /** 1.148 + * Adds a doorhanger. 1.149 + * 1.150 + * This method must be called on the UI thread. 1.151 + */ 1.152 + void addDoorHanger(final int tabId, final String value, final String message, 1.153 + final JSONArray buttons, final JSONObject options) { 1.154 + // Don't add a doorhanger for a tab that doesn't exist 1.155 + if (Tabs.getInstance().getTab(tabId) == null) { 1.156 + return; 1.157 + } 1.158 + 1.159 + // Replace the doorhanger if it already exists 1.160 + DoorHanger oldDoorHanger = getDoorHanger(tabId, value); 1.161 + if (oldDoorHanger != null) { 1.162 + removeDoorHanger(oldDoorHanger); 1.163 + } 1.164 + 1.165 + if (!mInflated) { 1.166 + init(); 1.167 + } 1.168 + 1.169 + final DoorHanger newDoorHanger = new DoorHanger(mActivity, tabId, value); 1.170 + newDoorHanger.setMessage(message); 1.171 + newDoorHanger.setOptions(options); 1.172 + 1.173 + for (int i = 0; i < buttons.length(); i++) { 1.174 + try { 1.175 + JSONObject buttonObject = buttons.getJSONObject(i); 1.176 + String label = buttonObject.getString("label"); 1.177 + String tag = String.valueOf(buttonObject.getInt("callback")); 1.178 + newDoorHanger.addButton(label, tag, this); 1.179 + } catch (JSONException e) { 1.180 + Log.e(LOGTAG, "Error creating doorhanger button", e); 1.181 + } 1.182 + } 1.183 + 1.184 + mDoorHangers.add(newDoorHanger); 1.185 + mContent.addView(newDoorHanger); 1.186 + 1.187 + // Only update the popup if we're adding a notifcation to the selected tab 1.188 + if (tabId == Tabs.getInstance().getSelectedTab().getId()) 1.189 + updatePopup(); 1.190 + } 1.191 + 1.192 + 1.193 + /* 1.194 + * DoorHanger.OnButtonClickListener implementation 1.195 + */ 1.196 + @Override 1.197 + public void onButtonClick(DoorHanger dh, String tag) { 1.198 + JSONObject response = new JSONObject(); 1.199 + try { 1.200 + response.put("callback", tag); 1.201 + 1.202 + CheckBox checkBox = dh.getCheckBox(); 1.203 + // If the checkbox is being used, pass its value 1.204 + if (checkBox != null) { 1.205 + response.put("checked", checkBox.isChecked()); 1.206 + } 1.207 + 1.208 + List<PromptInput> doorHangerInputs = dh.getInputs(); 1.209 + if (doorHangerInputs != null) { 1.210 + JSONObject inputs = new JSONObject(); 1.211 + for (PromptInput input : doorHangerInputs) { 1.212 + inputs.put(input.getId(), input.getValue()); 1.213 + } 1.214 + response.put("inputs", inputs); 1.215 + } 1.216 + } catch (JSONException e) { 1.217 + Log.e(LOGTAG, "Error creating onClick response", e); 1.218 + } 1.219 + 1.220 + GeckoEvent e = GeckoEvent.createBroadcastEvent("Doorhanger:Reply", response.toString()); 1.221 + GeckoAppShell.sendEventToGecko(e); 1.222 + removeDoorHanger(dh); 1.223 + updatePopup(); 1.224 + } 1.225 + 1.226 + /** 1.227 + * Gets a doorhanger. 1.228 + * 1.229 + * This method must be called on the UI thread. 1.230 + */ 1.231 + DoorHanger getDoorHanger(int tabId, String value) { 1.232 + for (DoorHanger dh : mDoorHangers) { 1.233 + if (dh.getTabId() == tabId && dh.getValue().equals(value)) 1.234 + return dh; 1.235 + } 1.236 + 1.237 + // If there's no doorhanger for the given tabId and value, return null 1.238 + return null; 1.239 + } 1.240 + 1.241 + /** 1.242 + * Removes a doorhanger. 1.243 + * 1.244 + * This method must be called on the UI thread. 1.245 + */ 1.246 + void removeDoorHanger(final DoorHanger doorHanger) { 1.247 + mDoorHangers.remove(doorHanger); 1.248 + mContent.removeView(doorHanger); 1.249 + } 1.250 + 1.251 + /** 1.252 + * Removes doorhangers for a given tab. 1.253 + * 1.254 + * This method must be called on the UI thread. 1.255 + */ 1.256 + void removeTransientDoorHangers(int tabId) { 1.257 + // Make a temporary set to avoid a ConcurrentModificationException 1.258 + HashSet<DoorHanger> doorHangersToRemove = new HashSet<DoorHanger>(); 1.259 + for (DoorHanger dh : mDoorHangers) { 1.260 + // Only remove transient doorhangers for the given tab 1.261 + if (dh.getTabId() == tabId && dh.shouldRemove(isShowing())) 1.262 + doorHangersToRemove.add(dh); 1.263 + } 1.264 + 1.265 + for (DoorHanger dh : doorHangersToRemove) { 1.266 + removeDoorHanger(dh); 1.267 + } 1.268 + } 1.269 + 1.270 + /** 1.271 + * Updates the popup state. 1.272 + * 1.273 + * This method must be called on the UI thread. 1.274 + */ 1.275 + void updatePopup() { 1.276 + // Bail if the selected tab is null, if there are no active doorhangers, 1.277 + // if we haven't inflated the layout yet (this can happen if updatePopup() 1.278 + // is called before the runnable from addDoorHanger() runs), or if the 1.279 + // doorhanger popup is temporarily disabled. 1.280 + Tab tab = Tabs.getInstance().getSelectedTab(); 1.281 + if (tab == null || mDoorHangers.size() == 0 || !mInflated || mDisabled) { 1.282 + dismiss(); 1.283 + return; 1.284 + } 1.285 + 1.286 + // Show doorhangers for the selected tab 1.287 + int tabId = tab.getId(); 1.288 + boolean shouldShowPopup = false; 1.289 + for (DoorHanger dh : mDoorHangers) { 1.290 + if (dh.getTabId() == tabId) { 1.291 + dh.setVisibility(View.VISIBLE); 1.292 + shouldShowPopup = true; 1.293 + } else { 1.294 + dh.setVisibility(View.GONE); 1.295 + } 1.296 + } 1.297 + 1.298 + // Dismiss the popup if there are no doorhangers to show for this tab 1.299 + if (!shouldShowPopup) { 1.300 + dismiss(); 1.301 + return; 1.302 + } 1.303 + 1.304 + showDividers(); 1.305 + if (isShowing()) { 1.306 + show(); 1.307 + return; 1.308 + } 1.309 + 1.310 + // Make the popup focusable for accessibility. This gets done here 1.311 + // so the node can be accessibility focused, but on pre-ICS devices this 1.312 + // causes crashes, so it is done after the popup is shown. 1.313 + if (Build.VERSION.SDK_INT >= 14) { 1.314 + setFocusable(true); 1.315 + } 1.316 + 1.317 + show(); 1.318 + 1.319 + if (Build.VERSION.SDK_INT < 14) { 1.320 + // Make the popup focusable for keyboard accessibility. 1.321 + setFocusable(true); 1.322 + } 1.323 + } 1.324 + 1.325 + //Show all inter-DoorHanger dividers (ie. Dividers on all visible DoorHangers except the last one) 1.326 + private void showDividers() { 1.327 + int count = mContent.getChildCount(); 1.328 + DoorHanger lastVisibleDoorHanger = null; 1.329 + 1.330 + for (int i = 0; i < count; i++) { 1.331 + DoorHanger dh = (DoorHanger) mContent.getChildAt(i); 1.332 + dh.showDivider(); 1.333 + if (dh.getVisibility() == View.VISIBLE) { 1.334 + lastVisibleDoorHanger = dh; 1.335 + } 1.336 + } 1.337 + if (lastVisibleDoorHanger != null) { 1.338 + lastVisibleDoorHanger.hideDivider(); 1.339 + } 1.340 + } 1.341 + 1.342 + private void registerEventListener(String event) { 1.343 + GeckoAppShell.getEventDispatcher().registerEventListener(event, this); 1.344 + } 1.345 + 1.346 + private void unregisterEventListener(String event) { 1.347 + GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this); 1.348 + } 1.349 + 1.350 + @Override 1.351 + public void dismiss() { 1.352 + // If the popup is focusable while it is hidden, we run into crashes 1.353 + // on pre-ICS devices when the popup gets focus before it is shown. 1.354 + setFocusable(false); 1.355 + super.dismiss(); 1.356 + } 1.357 +}