# HG changeset patch # Parent 1685afdada81ffccd2e41f1d8284404f67d08e2b # User Gian-Carlo Pascutto Add doorhanger notifications for Java UI. r= diff --git a/embedding/android/DoorHanger.java b/embedding/android/DoorHanger.java new file mode 100644 --- /dev/null +++ b/embedding/android/DoorHanger.java @@ -0,0 +1,81 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Gian-Carlo Pascutto + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko; + +import java.util.ArrayList; + +import android.util.Log; +import android.content.Context; +import android.widget.PopupWindow; +import android.widget.LinearLayout; + +public class DoorHanger { + private Context mContext; + public ArrayList mPopups; + + public DoorHanger(Context aContext) { + mContext = aContext; + mPopups = new ArrayList(); + } + + public DoorHangerPopup getPopup() { + final DoorHangerPopup dhp = new DoorHangerPopup(mContext); + mPopups.add(dhp); + return dhp; + } + + public void redraw(int tabId) { + Log.i("DoorHanger", "Redraw: " + tabId); + int yoff = 0; + for (final DoorHangerPopup dhp : mPopups) { + if (dhp.mTabId == tabId) { + dhp.setOnDismissListener(new PopupWindow.OnDismissListener() { + @Override + public void onDismiss() { + mPopups.remove(mPopups.lastIndexOf(dhp)); + } + }); + dhp.showAtHeight(10 + yoff); + yoff += dhp.getHeight() + 10; + } else { + dhp.setOnDismissListener(null); + dhp.dismiss(); + } + } + } +} diff --git a/embedding/android/DoorHangerPopup.java b/embedding/android/DoorHangerPopup.java new file mode 100644 --- /dev/null +++ b/embedding/android/DoorHangerPopup.java @@ -0,0 +1,108 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Gian-Carlo Pascutto + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko; + +import android.content.Context; +import android.widget.TextView; +import android.widget.Button; +import android.widget.PopupWindow; +import android.view.Display; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.LayoutInflater; +import android.widget.LinearLayout; +import android.widget.LinearLayout.LayoutParams; + +public class DoorHangerPopup extends PopupWindow { + private Context mContext; + private LinearLayout mChoicesLayout; + private TextView mTextView; + private Button mButton; + private LayoutParams mLayoutParams; + private View popupView; + public int mTabId; + + public DoorHangerPopup(Context aContext) { + super(aContext); + mContext = aContext; + + LayoutInflater inflater = + (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + popupView = (View) inflater.inflate(R.layout.doorhangerpopup, null); + setContentView(popupView); + + mChoicesLayout = (LinearLayout) popupView.findViewById(R.id.doorhanger_choices); + mTextView = (TextView) popupView.findViewById(R.id.doorhanger_title); + + mLayoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + } + + public void addButton(String aText, int aCallback) { + + final String sCallback = Integer.toString(aCallback); + Button mButton = new Button(mContext); + mButton.setText(aText); + mButton.setOnClickListener(new Button.OnClickListener() { + public void onClick(View v) { + GeckoEvent e = new GeckoEvent("doorhanger-reply", sCallback); + GeckoAppShell.sendEventToGecko(e); + dismiss(); + }}); + mChoicesLayout.addView(mButton, mLayoutParams); + } + + public void setText(String aText) { + mTextView.setText(aText); + } + + public void setTab(int tabId) { + mTabId = tabId; + } + + public void showAtHeight(int y) { + Display display = ((WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + + int width = display.getWidth(); + int height = display.getHeight(); + showAsDropDown(popupView); + update(0, height-110-y, width, 100); + } +} diff --git a/embedding/android/GeckoApp.java b/embedding/android/GeckoApp.java --- a/embedding/android/GeckoApp.java +++ b/embedding/android/GeckoApp.java @@ -87,16 +87,17 @@ abstract public class GeckoApp public static SurfaceView cameraView; public static GeckoApp mAppContext; public static boolean mFullscreen = false; public static File sGREDir = null; public Handler mMainHandler; private IntentFilter mConnectivityFilter; private BroadcastReceiver mConnectivityReceiver; private BrowserToolbar mBrowserToolbar; + public DoorHanger mDoorHanger; enum LaunchState {Launching, WaitButton, Launched, GeckoRunning, GeckoExiting}; private static LaunchState sLaunchState = LaunchState.Launching; private static boolean sTryCatchAttached = false; private static final int FILE_PICKER_REQUEST = 1; private static final int AWESOMEBAR_REQUEST = 2; @@ -593,16 +594,17 @@ abstract public class GeckoApp getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); setContentView(R.layout.gecko_app); mAppContext = this; // setup gecko layout mGeckoLayout = (RelativeLayout) findViewById(R.id.geckoLayout); mBrowserToolbar = (BrowserToolbar) findViewById(R.id.browserToolbar); + mDoorHanger = new DoorHanger(this); if (surfaceView == null) { surfaceView = new GeckoSurfaceView(this); mGeckoLayout.addView(surfaceView); } else if (mGeckoLayout.getChildCount() == 0) { //surfaceView still holds to the old one during rotation. re-add it to new activity ((ViewGroup) surfaceView.getParent()).removeAllViews(); mGeckoLayout.addView(surfaceView); @@ -1080,16 +1082,17 @@ abstract public class GeckoApp intent.putExtra(AwesomeBar.TYPE_KEY, AwesomeBar.Type.ADD.name()); startActivityForResult(intent, AWESOMEBAR_REQUEST); } else { int id = Integer.parseInt(data.getStringExtra(ShowTabs.ID)); Tab tab = Tabs.getInstance().selectTab(id); if (tab != null) { mBrowserToolbar.setTitle(tab.getTitle()); mBrowserToolbar.setProgressVisibility(tab.isLoading()); + mDoorHanger.redraw(id); } GeckoAppShell.sendEventToGecko(new GeckoEvent("tab-select", "" + id)); } } } } public void doCameraCapture() { diff --git a/embedding/android/GeckoAppShell.java b/embedding/android/GeckoAppShell.java --- a/embedding/android/GeckoAppShell.java +++ b/embedding/android/GeckoAppShell.java @@ -68,16 +68,17 @@ import android.net.Uri; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.graphics.Bitmap; import android.graphics.drawable.*; import org.json.JSONArray; import org.json.JSONObject; +import org.json.JSONException; public class GeckoAppShell { private static final String LOG_FILE_NAME = "GeckoAppShell"; // static members only private GeckoAppShell() { } @@ -1556,16 +1557,50 @@ public class GeckoAppShell if (sCamera != null) { sCamera.stopPreview(); sCamera.release(); sCamera = null; sCameraBuffer = null; } } + public static void handleDoorHanger(JSONObject geckoObject) throws JSONException { + final String msg = geckoObject.getString("message"); + final int tabId = geckoObject.getInt("tabID"); + Log.i("GeckoShell", "DoorHanger received for tab " + tabId + + ", msg:" + msg); + final JSONArray buttons = geckoObject.getJSONArray("buttons"); + + getMainHandler().post(new Runnable() { + public void run() { + DoorHangerPopup dhp = + GeckoApp.mAppContext.mDoorHanger.getPopup(); + dhp.setTab(tabId); + for (int i = 0; i < buttons.length(); i++) { + JSONObject jo; + String label; + int callBackId; + try { + jo = buttons.getJSONObject(i); + label = jo.getString("label"); + callBackId = jo.getInt("callback"); + Log.i("GeckoShell", "Label: " + label + + " CallbackId: " + callBackId); + dhp.addButton(label, callBackId); + } catch (JSONException e) { + Log.i("GeckoShell", "JSON throws " + e); + } + } + dhp.setText(msg); + int activeTab = Tabs.getInstance().getSelectedTabId(); + GeckoApp.mAppContext.mDoorHanger.redraw(activeTab); + } + }); + } + public static void handleGeckoMessage(String message) { // // {"gecko": { // "type": "value", // "event_specific": "value", // .... try { JSONObject json = new JSONObject(message); @@ -1622,14 +1657,16 @@ public class GeckoAppShell } else if (type.equals("onCameraCapture")) { //GeckoApp.mAppContext.doCameraCapture(geckoObject.getString("path")); GeckoApp.mAppContext.doCameraCapture(); } else if (type.equals("onCreateTab")) { Log.i("GeckoShell", "Created a new tab"); int tabId = geckoObject.getInt("tabID"); String uri = geckoObject.getString("uri"); Tabs.getInstance().addTab(tabId, uri); + } else if (type.equals("doorhanger")) { + handleDoorHanger(geckoObject); } } catch (Exception e) { Log.i("GeckoShell", "handleGeckoMessage throws " + e); } } } diff --git a/embedding/android/Makefile.in b/embedding/android/Makefile.in --- a/embedding/android/Makefile.in +++ b/embedding/android/Makefile.in @@ -55,16 +55,18 @@ JAVAFILES = \ GeckoInputConnection.java \ AlertNotification.java \ SurfaceLockInfo.java \ AwesomeBar.java \ GeckoBookmarks.java \ Tab.java \ Tabs.java \ ShowTabs.java \ + DoorHanger.java \ + DoorHangerPopup.java \ $(NULL) PROCESSEDJAVAFILES = \ App.java \ Restarter.java \ NotificationHandler.java \ LauncherShortcuts.java \ $(NULL) @@ -134,16 +136,17 @@ RES_LAYOUT = \ res/layout/launch_app_list.xml \ res/layout/launch_app_listitem.xml \ res/layout/awesomebar_search.xml \ res/layout/awesomebar_row.xml \ res/layout/browser_toolbar.xml \ res/layout/bookmarks.xml \ res/layout/bookmark_list_row.xml \ res/layout/show_tabs.xml \ + res/layout/doorhangerpopup.xml \ $(NULL) RES_VALUES = \ res/values/colors.xml \ res/values/styles.xml \ res/values/themes.xml \ $(NULL) diff --git a/embedding/android/resources/layout/doorhangerpopup.xml b/embedding/android/resources/layout/doorhangerpopup.xml new file mode 100644 --- /dev/null +++ b/embedding/android/resources/layout/doorhangerpopup.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/mobile/components/ContentPermissionPrompt.js b/mobile/components/ContentPermissionPrompt.js --- a/mobile/components/ContentPermissionPrompt.js +++ b/mobile/components/ContentPermissionPrompt.js @@ -1,17 +1,61 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is mozilla.org code + * + * The Initial Developer of the Original Code is + * Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Gian-Carlo Pascutto + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; +const Cc = Components.classes; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); const kCountBeforeWeRemember = 5; +function dump(a) { + Cc["@mozilla.org/consoleservice;1"] + .getService(Ci.nsIConsoleService) + .logStringMessage(a); +} + function setPagePermission(type, uri, allow) { let pm = Services.perms; let contentPrefs = Services.contentPrefs; let contentPrefName = type + ".request.remember"; if (!contentPrefs.hasPref(uri, contentPrefName)) contentPrefs.setPref(uri, contentPrefName, 0); @@ -35,91 +79,160 @@ const kEntities = { "geolocation": "geol function ContentPermissionPrompt() {} ContentPermissionPrompt.prototype = { classID: Components.ID("{C6E8C44D-9F39-4AF7-BCC0-76E38A8310F5}"), QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt]), - getChromeWindow: function getChromeWindow(aWindow) { - let chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIDocShellTreeItem) - .rootTreeItem - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindow) - .QueryInterface(Ci.nsIDOMChromeWindow); - return chromeWin; - }, - - getNotificationBoxForRequest: function getNotificationBoxForRequest(request) { - let notificationBox = null; - if (request.window) { - let requestingWindow = request.window.top; - let chromeWin = this.getChromeWindow(requestingWindow).wrappedJSObject; - let windowID = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID; - let browser = chromeWin.Browser.getBrowserForWindowId(windowID); - return chromeWin.getNotificationBox(browser); - } - - let chromeWin = request.element.ownerDocument.defaultView; - return chromeWin.Browser.getNotificationBox(request.element); - }, + _promptId : 0, + _callBackId : 0, + _callBackStack : [], handleExistingPermission: function handleExistingPermission(request) { let result = Services.perms.testExactPermission(request.uri, request.type); if (result == Ci.nsIPermissionManager.ALLOW_ACTION) { request.allow(); return true; } if (result == Ci.nsIPermissionManager.DENY_ACTION) { request.cancel(); return true; } return false; }, + getChromeWindow: function getChromeWindow(aWindow) { + let chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow) + .QueryInterface(Ci.nsIDOMChromeWindow); + return chromeWin; + }, + + getTabForRequest: function getTabForRequest(request) { + if (request.window) { + let requestingWindow = request.window.top; + let chromeWin = this.getChromeWindow(requestingWindow).wrappedJSObject; + let windowID = chromeWin.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID; + let browser = chromeWin.BrowserApp.getBrowserForWindow(request.window); + let tabID = chromeWin.BrowserApp.getTabForBrowser(browser).id; + return tabID; + } + let chromeWin = request.element.ownerDocument.defaultView; + let browser = chromeWin.Browser; + let tabID = chromeWin.BrowserApp.getTabForBrowser(browser).id; + return tabID; + }, + prompt: function(request) { // returns true if the request was handled if (this.handleExistingPermission(request)) return; let pm = Services.perms; - let notificationBox = this.getNotificationBoxForRequest(request); let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties"); - - let notification = notificationBox.getNotificationWithValue(request.type); - if (notification) - return; let entityName = kEntities[request.type]; + let tabID = this.getTabForRequest(request); + dump("Notifying tabId: " + tabID); + + this._promptId++; + this._callBackId++; + let allowCallback = { + cb : function(notification) { + setPagePermission(request.type, request.uri, true); + request.allow(); + }, + callBackId : this._callBackId, + promptId : this._promptId + }; + this._callBackId++; + let denyCallback = { + cb : function(notification) { + setPagePermission(request.type, request.uri, false); + request.cancel(); + }, + callBackId : this._callBackId, + promptId : this._promptId + }; + this._callBackStack.push(allowCallback); + this._callBackStack.push(denyCallback); + let buttons = [{ label: browserBundle.GetStringFromName(entityName + ".allow"), accessKey: null, - callback: function(notification) { - setPagePermission(request.type, request.uri, true); - request.allow(); - } + callback: allowCallback.callBackId }, { label: browserBundle.GetStringFromName(entityName + ".dontAllow"), accessKey: null, - callback: function(notification) { - setPagePermission(request.type, request.uri, false); - request.cancel(); - } + callback: denyCallback.callBackId }]; let message = browserBundle.formatStringFromName(entityName + ".wantsTo", [request.uri.host], 1); - let newBar = notificationBox.appendNotification(message, - request.type, - "", // Notifications in Fennec do not display images. - notificationBox.PRIORITY_WARNING_MEDIUM, - buttons); + + dump("Notifying: " + message); + + let DoorhangerEventListener = { + _ContentPermissionObj : {}, + init: function(owner) { + Services.obs.addObserver(this, "doorhanger-reply", false); + this._ContentPermissionObj = owner; + }, + observe: function(aSubject, aTopic, aData) { + let cpo = this._ContentPermissionObj; + if (aTopic == "doorhanger-reply") { + dump("DoorHanger-reply topic: " + aTopic + " data: " + aData); + let data = JSON.parse(aData); + let cbId = parseInt(data); + let promptId = -1; + let keepStack = []; + // Find the callback to call for this id + for (i = 0; i < cpo._callBackStack.length; i++) { + if (cpo._callBackStack[i].callBackId == cbId) { + promptId = cpo._callBackStack[i].promptId; + cpo._callBackStack[i].cb(); + break; + } + } + // Now find all remaining callbacks that were not + // in the same notification (!same promptId) + for (i = 0; i < cpo._callBackStack.length; i++) { + if (cpo._callBackStack[i].promptId != promptId) { + keepStack.push(cpo._callBackStack[i]); + } + } + // Keep those, throw away everything else + cpo._callBackStack = keepStack; + if (cpo._callBackStack.length == 0) { + // Remove if this was the last one outstanding + Services.obs.removeObserver(this, "doorhanger-reply"); + } + } + }, + }; + DoorhangerEventListener.init(this); + + let JavaMessage = { + "gecko": { + "type" : "doorhanger", + "message" : message, + "severity" : "PRIORITY_WARNING_MEDIUM", + "buttons" : buttons, + "tabID" : tabID + }}; + + Cc["@mozilla.org/android/bridge;1"] + .getService(Ci.nsIAndroidBridge) + .handleGeckoMessage(JSON.stringify(JavaMessage)); } }; //module initialization const NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentPermissionPrompt]);