author | Carsten "Tomcat" Book <cbook@mozilla.com> |
Thu, 07 Nov 2013 14:24:24 +0100 | |
changeset 153954 | f73dd492c34c1ea7a6f7a08c78c0105fe70673cc |
parent 153910 | 21b77163bf9f12b25dcfee04d36edc2c7f809f1d (current diff) |
parent 153953 | 9f1168eae1b782e2a511c90c49cc3fca8742ab07 (diff) |
child 153988 | 7433abfef863802979912bb538fa2b579dfaf422 |
push id | 35945 |
push user | cbook@mozilla.com |
push date | Thu, 07 Nov 2013 15:08:08 +0000 |
treeherder | mozilla-inbound@5251ff6456d9 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
milestone | 28.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
|
Makefile.in | file | annotate | diff | comparison | revisions |
--- a/Makefile.in +++ b/Makefile.in @@ -67,17 +67,17 @@ config.status: $(topsrcdir)/configure # this main make file because having it in rules.mk and applied to partial tree # builds resulted in a world of hurt. Gory details are in bug 877308. # # The mach build driver will ensure the backend is up to date for partial tree # builds. This cleanly avoids most of the pain. backend.RecursiveMakeBackend: @echo "Build configuration changed. Regenerating backend." - ./config.status + $(PYTHON) config.status Makefile: backend.RecursiveMakeBackend @$(TOUCH) $@ include backend.RecursiveMakeBackend.pp default:: backend.RecursiveMakeBackend
--- a/dom/webidl/Document.webidl +++ b/dom/webidl/Document.webidl @@ -146,17 +146,17 @@ partial interface Document { attribute EventHandler oncut; attribute EventHandler onpaste; attribute EventHandler onbeforescriptexecute; attribute EventHandler onafterscriptexecute; /** * True if this document is synthetic : stand alone image, video, audio file, * etc. */ - [ChromeOnly] readonly attribute boolean mozSyntheticDocument; + [Func="IsChromeOrXBL"] readonly attribute boolean mozSyntheticDocument; /** * Returns the script element whose script is currently being processed. * * @see <https://developer.mozilla.org/en/DOM/document.currentScript> */ readonly attribute Element? currentScript; /** * Release the current mouse capture if it is on an element within this
--- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -707,28 +707,43 @@ abstract public class BrowserApp extends if (itemId == R.id.share) { shareCurrentUrl(); return true; } if (itemId == R.id.subscribe) { Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab != null && tab.getFeedsEnabled()) { + if (tab != null && tab.hasFeeds()) { JSONObject args = new JSONObject(); try { args.put("tabId", tab.getId()); } catch (JSONException e) { Log.e(LOGTAG, "error building json arguments"); } GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Feeds:Subscribe", args.toString())); } return true; } + if (itemId == R.id.add_search_engine) { + Tab tab = Tabs.getInstance().getSelectedTab(); + if (tab != null && tab.hasOpenSearch()) { + JSONObject args = new JSONObject(); + try { + args.put("tabId", tab.getId()); + } catch (JSONException e) { + Log.e(LOGTAG, "error building json arguments"); + return true; + } + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:Add", args.toString())); + } + return true; + } + if (itemId == R.id.copyurl) { Tab tab = Tabs.getInstance().getSelectedTab(); if (tab != null) { String url = tab.getURL(); if (url != null) { Clipboard.setText(url); } }
--- a/mobile/android/base/BrowserToolbar.java +++ b/mobile/android/base/BrowserToolbar.java @@ -372,25 +372,29 @@ public class BrowserToolbar extends Geck Tab tab = Tabs.getInstance().getSelectedTab(); if (tab != null) { String url = tab.getURL(); if (url == null) { menu.findItem(R.id.copyurl).setVisible(false); menu.findItem(R.id.share).setVisible(false); menu.findItem(R.id.add_to_launcher).setVisible(false); } - if (!tab.getFeedsEnabled()) { + + if (!tab.hasFeeds()) { menu.findItem(R.id.subscribe).setVisible(false); } + + menu.findItem(R.id.add_search_engine).setVisible(tab.hasOpenSearch()); } else { // if there is no tab, remove anything tab dependent menu.findItem(R.id.copyurl).setVisible(false); menu.findItem(R.id.share).setVisible(false); menu.findItem(R.id.add_to_launcher).setVisible(false); menu.findItem(R.id.subscribe).setVisible(false); + menu.findItem(R.id.add_search_engine).setVisible(false); } menu.findItem(R.id.share).setVisible(!GeckoProfile.get(getContext()).inGuestMode()); } }); mUrlEditText.addTextChangedListener(this);
--- a/mobile/android/base/GeckoView.java +++ b/mobile/android/base/GeckoView.java @@ -8,37 +8,45 @@ package org.mozilla.gecko; import org.mozilla.gecko.db.BrowserDB; import org.mozilla.gecko.gfx.LayerView; import org.mozilla.gecko.mozglue.GeckoLoader; import org.mozilla.gecko.util.Clipboard; import org.mozilla.gecko.util.HardwareUtils; import org.mozilla.gecko.util.GeckoEventListener; import org.mozilla.gecko.util.ThreadUtils; +import org.json.JSONException; import org.json.JSONObject; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.util.AttributeSet; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + public class GeckoView extends LayerView implements GeckoEventListener, ContextGetter { private static final String LOGTAG = "GeckoView"; + private ChromeDelegate mChromeDelegate; + private ContentDelegate mContentDelegate; + public GeckoView(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GeckoView); String url = a.getString(R.styleable.GeckoView_url); boolean doInit = a.getBoolean(R.styleable.GeckoView_doinit, true); a.recycle(); if (!doInit) @@ -71,52 +79,476 @@ public class GeckoView extends LayerView GeckoThread.setAction(Intent.ACTION_VIEW); GeckoAppShell.sendEventToGecko(GeckoEvent.createURILoadEvent(url)); } GeckoAppShell.setContextGetter(this); if (context instanceof Activity) { Tabs tabs = Tabs.getInstance(); tabs.attachToContext(context); } + GeckoAppShell.registerEventListener("Gecko:Ready", this); + GeckoAppShell.registerEventListener("Content:StateChange", this); + GeckoAppShell.registerEventListener("Content:LoadError", this); + GeckoAppShell.registerEventListener("Content:PageShow", this); + GeckoAppShell.registerEventListener("DOMTitleChanged", this); + GeckoAppShell.registerEventListener("Link:Favicon", this); + GeckoAppShell.registerEventListener("Prompt:Show", this); + GeckoAppShell.registerEventListener("Prompt:ShowTop", this); ThreadUtils.setUiThread(Thread.currentThread(), new Handler()); initializeView(GeckoAppShell.getEventDispatcher()); GeckoProfile profile = GeckoProfile.get(context).forceCreate(); BrowserDB.initialize(profile.getName()); if (GeckoThread.checkAndSetLaunchState(GeckoThread.LaunchState.Launching, GeckoThread.LaunchState.Launched)) { GeckoAppShell.setLayerView(this); GeckoThread.createAndStart(); } } - public void loadUrl(String uri) { - Tabs.getInstance().loadUrl(uri); + /** + * Add a Browser to the GeckoView container. + * @param url The URL resource to load into the new Browser. + */ + public Browser addBrowser(String url) { + Tab tab = Tabs.getInstance().loadUrl(url, Tabs.LOADURL_NEW_TAB); + if (tab != null) { + return new Browser(tab.getId()); + } + return null; + } + + /** + * Remove a Browser from the GeckoView container. + * @param browser The Browser to remove. + */ + public void removeBrowser(Browser browser) { + Tab tab = Tabs.getInstance().getTab(browser.getId()); + if (tab != null) { + Tabs.getInstance().closeTab(tab); + } + } + + /** + * Set the active/visible Browser. + * @param browser The Browser to make selected. + */ + public void setCurrentBrowser(Browser browser) { + Tab tab = Tabs.getInstance().getTab(browser.getId()); + if (tab != null) { + Tabs.getInstance().selectTab(tab.getId()); + } + } + + /** + * Get the active/visible Browser. + * @return The current selected Browser. + */ + public Browser getCurrentBrowser() { + Tab tab = Tabs.getInstance().getSelectedTab(); + if (tab != null) { + return new Browser(tab.getId()); + } + return null; + } + + /** + * Get the list of current Browsers in the GeckoView container. + * @return An unmodifiable List of Browser objects. + */ + public List<Browser> getBrowsers() { + ArrayList<Browser> browsers = new ArrayList<Browser>(); + Iterable<Tab> tabs = Tabs.getInstance().getTabsInOrder(); + for (Tab tab : tabs) { + browsers.add(new Browser(tab.getId())); + } + return Collections.unmodifiableList(browsers); + } + + /** + * Not part of the public API. Ignore. + */ + public void handleMessage(final String event, final JSONObject message) { + ThreadUtils.postToUiThread(new Runnable() { + @Override + public void run() { + try { + if (event.equals("Gecko:Ready")) { + GeckoView.this.handleReady(message); + } else if (event.equals("Content:StateChange")) { + GeckoView.this.handleStateChange(message); + } else if (event.equals("Content:LoadError")) { + GeckoView.this.handleLoadError(message); + } else if (event.equals("Content:PageShow")) { + GeckoView.this.handlePageShow(message); + } else if (event.equals("DOMTitleChanged")) { + GeckoView.this.handleTitleChanged(message); + } else if (event.equals("Link:Favicon")) { + GeckoView.this.handleLinkFavicon(message); + } else if (event.equals("Prompt:Show") || event.equals("Prompt:ShowTop")) { + GeckoView.this.handlePrompt(message); + } + } catch (Exception e) { + Log.w(LOGTAG, "handleMessage threw for " + event, e); + } + } + }); } - public void loadUrlInNewTab(String uri) { - Tabs.getInstance().loadUrl(uri, Tabs.LOADURL_NEW_TAB); - } + private void handleReady(final JSONObject message) { + GeckoThread.setLaunchState(GeckoThread.LaunchState.GeckoRunning); + Tab selectedTab = Tabs.getInstance().getSelectedTab(); + if (selectedTab != null) + Tabs.getInstance().notifyListeners(selectedTab, Tabs.TabEvents.SELECTED); + geckoConnected(); + GeckoAppShell.setLayerClient(getLayerClient()); + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Viewport:Flush", null)); + show(); + requestRender(); + + if (mChromeDelegate != null) { + mChromeDelegate.onReady(this); + } + } + + private void handleStateChange(final JSONObject message) throws JSONException { + int state = message.getInt("state"); + if ((state & GeckoAppShell.WPL_STATE_IS_NETWORK) != 0) { + if ((state & GeckoAppShell.WPL_STATE_START) != 0) { + if (mContentDelegate != null) { + int id = message.getInt("tabID"); + mContentDelegate.onPageStart(this, new Browser(id), message.getString("uri")); + } + } else if ((state & GeckoAppShell.WPL_STATE_STOP) != 0) { + if (mContentDelegate != null) { + int id = message.getInt("tabID"); + mContentDelegate.onPageStop(this, new Browser(id), message.getBoolean("success")); + } + } + } + } + + private void handleLoadError(final JSONObject message) throws JSONException { + if (mContentDelegate != null) { + int id = message.getInt("tabID"); + mContentDelegate.onPageStop(GeckoView.this, new Browser(id), false); + } + } + + private void handlePageShow(final JSONObject message) throws JSONException { + if (mContentDelegate != null) { + int id = message.getInt("tabID"); + mContentDelegate.onPageShow(GeckoView.this, new Browser(id)); + } + } - public void handleMessage(String event, JSONObject message) { - if (event.equals("Gecko:Ready")) { - GeckoThread.setLaunchState(GeckoThread.LaunchState.GeckoRunning); - Tab selectedTab = Tabs.getInstance().getSelectedTab(); - if (selectedTab != null) - Tabs.getInstance().notifyListeners(selectedTab, Tabs.TabEvents.SELECTED); - geckoConnected(); - GeckoAppShell.setLayerClient(getLayerClient()); - GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Viewport:Flush", null)); - show(); - requestRender(); + private void handleTitleChanged(final JSONObject message) throws JSONException { + if (mContentDelegate != null) { + int id = message.getInt("tabID"); + mContentDelegate.onReceivedTitle(GeckoView.this, new Browser(id), message.getString("title")); + } + } + + private void handleLinkFavicon(final JSONObject message) throws JSONException { + if (mContentDelegate != null) { + int id = message.getInt("tabID"); + mContentDelegate.onReceivedFavicon(GeckoView.this, new Browser(id), message.getString("href"), message.getInt("size")); } } + private void handlePrompt(final JSONObject message) throws JSONException { + if (mChromeDelegate != null) { + String hint = message.optString("hint"); + if ("alert".equals(hint)) { + String text = message.optString("text"); + mChromeDelegate.onAlert(GeckoView.this, null, text, new PromptResult(message.optString("guid"))); + } else if ("confirm".equals(hint)) { + String text = message.optString("text"); + mChromeDelegate.onConfirm(GeckoView.this, null, text, new PromptResult(message.optString("guid"))); + } else if ("prompt".equals(hint)) { + String text = message.optString("text"); + String defaultValue = message.optString("textbox0"); + mChromeDelegate.onPrompt(GeckoView.this, null, text, defaultValue, new PromptResult(message.optString("guid"))); + } else if ("remotedebug".equals(hint)) { + mChromeDelegate.onDebugRequest(GeckoView.this, new PromptResult(message.optString("guid"))); + } + } + } + + /** + * Set the chrome callback handler. + * This will replace the current handler. + * @param chrome An implementation of GeckoViewChrome. + */ + public void setChromeDelegate(ChromeDelegate chrome) { + mChromeDelegate = chrome; + } + + /** + * Set the content callback handler. + * This will replace the current handler. + * @param content An implementation of ContentDelegate. + */ + public void setContentDelegate(ContentDelegate content) { + mContentDelegate = content; + } + public static void setGeckoInterface(final BaseGeckoInterface geckoInterface) { GeckoAppShell.setGeckoInterface(geckoInterface); } public static GeckoAppShell.GeckoInterface getGeckoInterface() { return GeckoAppShell.getGeckoInterface(); } + + /** + * Wrapper for a browser in the GeckoView container. Associated with a browser + * element in the Gecko system. + */ + public class Browser { + private final int mId; + private Browser(int Id) { + mId = Id; + } + + /** + * Get the ID of the Browser. This is the same ID used by Gecko for it's underlying + * browser element. + * @return The integer ID of the Browser. + */ + private int getId() { + return mId; + } + + /** + * Load a URL resource into the Browser. + * @param url The URL string. + */ + public void loadUrl(String url) { + JSONObject args = new JSONObject(); + try { + args.put("url", url); + args.put("parentId", -1); + args.put("newTab", false); + args.put("tabID", mId); + } catch (Exception e) { + Log.w(LOGTAG, "Error building JSON arguments for loadUrl.", e); + } + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Load", args.toString())); + } + + /** + * Reload the current URL resource into the Browser. The URL is force loaded from the + * network and is not pulled from cache. + */ + public void reload() { + Tab tab = Tabs.getInstance().getTab(mId); + if (tab != null) { + tab.doReload(); + } + } + + /** + * Stop the current loading operation. + */ + public void stop() { + Tab tab = Tabs.getInstance().getTab(mId); + if (tab != null) { + tab.doStop(); + } + } + + /** + * Check to see if the Browser has session history and can go back to a + * previous page. + * @return A boolean flag indicating if previous session exists. + * This method will likely be removed and replaced by a callback in GeckoViewContent + */ + public boolean canGoBack() { + Tab tab = Tabs.getInstance().getTab(mId); + if (tab != null) { + return tab.canDoBack(); + } + return false; + } + + /** + * Move backward in the session history, if that's possible. + */ + public void goBack() { + Tab tab = Tabs.getInstance().getTab(mId); + if (tab != null) { + tab.doBack(); + } + } + + /** + * Check to see if the Browser has session history and can go forward to a + * new page. + * @return A boolean flag indicating if forward session exists. + * This method will likely be removed and replaced by a callback in GeckoViewContent + */ + public boolean canGoForward() { + Tab tab = Tabs.getInstance().getTab(mId); + if (tab != null) { + return tab.canDoForward(); + } + return false; + } + + /** + * Move forward in the session history, if that's possible. + */ + public void goForward() { + Tab tab = Tabs.getInstance().getTab(mId); + if (tab != null) { + tab.doForward(); + } + } + } + + /* Provides a means for the client to indicate whether a JavaScript + * dialog request should proceed. An instance of this class is passed to + * various GeckoViewChrome callback actions. + */ + public class PromptResult { + private final int RESULT_OK = 0; + private final int RESULT_CANCEL = 1; + + private final String mGUID; + + public PromptResult(String guid) { + mGUID = guid; + } + + private JSONObject makeResult(int resultCode) { + JSONObject result = new JSONObject(); + try { + result.put("guid", mGUID); + result.put("button", resultCode); + } catch(JSONException ex) { } + return result; + } + + /** + * Handle a confirmation response from the user. + */ + public void confirm() { + JSONObject result = makeResult(RESULT_OK); + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Prompt:Reply", result.toString())); + } + + /** + * Handle a confirmation response from the user. + * @param value String value to return to the browser context. + */ + public void confirmWithValue(String value) { + JSONObject result = makeResult(RESULT_OK); + try { + result.put("textbox0", value); + } catch(JSONException ex) { } + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Prompt:Reply", result.toString())); + } + + /** + * Handle a cancellation response from the user. + */ + public void cancel() { + JSONObject result = makeResult(RESULT_CANCEL); + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Prompt:Reply", result.toString())); + } + } + + public interface ChromeDelegate { + /** + * Tell the host application that Gecko is ready to handle requests. + * @param view The GeckoView that initiated the callback. + */ + public void onReady(GeckoView view); + + /** + * Tell the host application to display an alert dialog. + * @param view The GeckoView that initiated the callback. + * @param browser The Browser that is loading the content. + * @param message The string to display in the dialog. + * @param result A PromptResult used to send back the result without blocking. + * Defaults to cancel requests. + */ + public void onAlert(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result); + + /** + * Tell the host application to display a confirmation dialog. + * @param view The GeckoView that initiated the callback. + * @param browser The Browser that is loading the content. + * @param message The string to display in the dialog. + * @param result A PromptResult used to send back the result without blocking. + * Defaults to cancel requests. + */ + public void onConfirm(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result); + + /** + * Tell the host application to display an input prompt dialog. + * @param view The GeckoView that initiated the callback. + * @param browser The Browser that is loading the content. + * @param message The string to display in the dialog. + * @param defaultValue The string to use as default input. + * @param result A PromptResult used to send back the result without blocking. + * Defaults to cancel requests. + */ + public void onPrompt(GeckoView view, GeckoView.Browser browser, String message, String defaultValue, GeckoView.PromptResult result); + + /** + * Tell the host application to display a remote debugging request dialog. + * @param view The GeckoView that initiated the callback. + * @param result A PromptResult used to send back the result without blocking. + * Defaults to cancel requests. + */ + public void onDebugRequest(GeckoView view, GeckoView.PromptResult result); + } + + public interface ContentDelegate { + /** + * A Browser has started loading content from the network. + * @param view The GeckoView that initiated the callback. + * @param browser The Browser that is loading the content. + * @param url The resource being loaded. + */ + public void onPageStart(GeckoView view, GeckoView.Browser browser, String url); + + /** + * A Browser has finished loading content from the network. + * @param view The GeckoView that initiated the callback. + * @param browser The Browser that was loading the content. + * @param success Whether the page loaded successfully or an error occured. + */ + public void onPageStop(GeckoView view, GeckoView.Browser browser, boolean success); + + /** + * A Browser is displaying content. This page could have been loaded via + * network or from the session history. + * @param view The GeckoView that initiated the callback. + * @param browser The Browser that is showing the content. + */ + public void onPageShow(GeckoView view, GeckoView.Browser browser); + + /** + * A page title was discovered in the content or updated after the content + * loaded. + * @param view The GeckoView that initiated the callback. + * @param browser The Browser that is showing the content. + * @param title The title sent from the content. + */ + public void onReceivedTitle(GeckoView view, GeckoView.Browser browser, String title); + + /** + * A link element was discovered in the content or updated after the content + * loaded that specifies a favicon. + * @param view The GeckoView that initiated the callback. + * @param browser The Browser that is showing the content. + * @param url The href of the link element specifying the favicon. + * @param size The maximum size specified for the favicon, or -1 for any size. + */ + public void onReceivedFavicon(GeckoView view, GeckoView.Browser browser, String url, int size); + } + }
new file mode 100644 --- /dev/null +++ b/mobile/android/base/GeckoViewChrome.java @@ -0,0 +1,61 @@ +/* -*- 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; + +public class GeckoViewChrome implements GeckoView.ChromeDelegate { + /** + * Tell the host application that Gecko is ready to handle requests. + * @param view The GeckoView that initiated the callback. + */ + public void onReady(GeckoView view) {} + + /** + * Tell the host application to display an alert dialog. + * @param view The GeckoView that initiated the callback. + * @param browser The Browser that is loading the content. + * @param message The string to display in the dialog. + * @param result A PromptResult used to send back the result without blocking. + * Defaults to cancel requests. + */ + public void onAlert(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result) { + result.cancel(); + } + + /** + * Tell the host application to display a confirmation dialog. + * @param view The GeckoView that initiated the callback. + * @param browser The Browser that is loading the content. + * @param message The string to display in the dialog. + * @param result A PromptResult used to send back the result without blocking. + * Defaults to cancel requests. + */ + public void onConfirm(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result) { + result.cancel(); + } + + /** + * Tell the host application to display an input prompt dialog. + * @param view The GeckoView that initiated the callback. + * @param browser The Browser that is loading the content. + * @param message The string to display in the dialog. + * @param defaultValue The string to use as default input. + * @param result A PromptResult used to send back the result without blocking. + * Defaults to cancel requests. + */ + public void onPrompt(GeckoView view, GeckoView.Browser browser, String message, String defaultValue, GeckoView.PromptResult result) { + result.cancel(); + } + + /** + * Tell the host application to display a remote debugging request dialog. + * @param view The GeckoView that initiated the callback. + * @param result A PromptResult used to send back the result without blocking. + * Defaults to cancel requests. + */ + public void onDebugRequest(GeckoView view, GeckoView.PromptResult result) { + result.cancel(); + } +}
new file mode 100644 --- /dev/null +++ b/mobile/android/base/GeckoViewContent.java @@ -0,0 +1,51 @@ +/* -*- 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; + +public class GeckoViewContent implements GeckoView.ContentDelegate { + /** + * A Browser has started loading content from the network. + * @param view The GeckoView that initiated the callback. + * @param browser The Browser that is loading the content. + * @param url The resource being loaded. + */ + public void onPageStart(GeckoView view, GeckoView.Browser browser, String url) {} + + /** + * A Browser has finished loading content from the network. + * @param view The GeckoView that initiated the callback. + * @param browser The Browser that was loading the content. + * @param success Whether the page loaded successfully or an error occured. + */ + public void onPageStop(GeckoView view, GeckoView.Browser browser, boolean success) {} + + /** + * A Browser is displaying content. This page could have been loaded via + * network or from the session history. + * @param view The GeckoView that initiated the callback. + * @param browser The Browser that is showing the content. + */ + public void onPageShow(GeckoView view, GeckoView.Browser browser) {} + + /** + * A page title was discovered in the content or updated after the content + * loaded. + * @param view The GeckoView that initiated the callback. + * @param browser The Browser that is showing the content. + * @param title The title sent from the content. + */ + public void onReceivedTitle(GeckoView view, GeckoView.Browser browser, String title) {} + + /** + * A link element was discovered in the content or updated after the content + * loaded that specifies a favicon. + * @param view The GeckoView that initiated the callback. + * @param browser The Browser that is showing the content. + * @param url The href of the link element specifying the favicon. + * @param size The maximum size specified for the favicon, or -1 for any size. + */ + public void onReceivedFavicon(GeckoView view, GeckoView.Browser browser, String url, int size) {} +}
--- a/mobile/android/base/Tab.java +++ b/mobile/android/base/Tab.java @@ -38,17 +38,18 @@ public class Tab { private long mLastUsed; private String mUrl; private String mBaseDomain; private String mUserSearch; private String mTitle; private Bitmap mFavicon; private String mFaviconUrl; private int mFaviconSize; - private boolean mFeedsEnabled; + private boolean mHasFeeds; + private boolean mHasOpenSearch; private JSONObject mIdentityData; private boolean mReaderEnabled; private BitmapDrawable mThumbnail; private int mHistoryIndex; private int mHistorySize; private int mParentId; private HomePager.Page mAboutHomePage; private boolean mExternal; @@ -93,17 +94,18 @@ public class Tab { mUserSearch = ""; mExternal = external; mParentId = parentId; mAboutHomePage = HomePager.Page.TOP_SITES; mTitle = title == null ? "" : title; mFavicon = null; mFaviconUrl = null; mFaviconSize = 0; - mFeedsEnabled = false; + mHasFeeds = false; + mHasOpenSearch = false; mIdentityData = null; mReaderEnabled = false; mEnteringReaderMode = false; mThumbnail = null; mHistoryIndex = -1; mHistorySize = 0; mBookmark = false; mReadingListItem = false; @@ -231,18 +233,22 @@ public class Tab { } }); } public synchronized String getFaviconURL() { return mFaviconUrl; } - public boolean getFeedsEnabled() { - return mFeedsEnabled; + public boolean hasFeeds() { + return mHasFeeds; + } + + public boolean hasOpenSearch() { + return mHasOpenSearch; } public String getSecurityMode() { try { return mIdentityData.getString("mode"); } catch (Exception e) { // If mIdentityData is null, or we get a JSONException return SiteIdentityPopup.UNKNOWN; @@ -388,18 +394,22 @@ public class Tab { if (mEnteringReaderMode) return; mFavicon = null; mFaviconUrl = null; mFaviconSize = 0; } - public void setFeedsEnabled(boolean feedsEnabled) { - mFeedsEnabled = feedsEnabled; + public void setHasFeeds(boolean hasFeeds) { + mHasFeeds = hasFeeds; + } + + public void setHasOpenSearch(boolean hasOpenSearch) { + mHasOpenSearch = hasOpenSearch; } public void updateIdentityData(JSONObject identityData) { mIdentityData = identityData; } public void setReaderEnabled(boolean readerEnabled) { mReaderEnabled = readerEnabled; @@ -617,17 +627,17 @@ public class Tab { // We can get a location change event for the same document with an anchor tag // Notify listeners so that buttons like back or forward will update themselves Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.LOCATION_CHANGE, uri); return; } setContentType(message.getString("contentType")); clearFavicon(); - setFeedsEnabled(false); + setHasFeeds(false); updateTitle(null); updateIdentityData(null); setReaderEnabled(false); setZoomConstraints(new ZoomConstraints(true)); setHasTouchListeners(false); setBackgroundColor(DEFAULT_BACKGROUND_COLOR); setErrorType(ErrorType.NONE);
--- a/mobile/android/base/Tabs.java +++ b/mobile/android/base/Tabs.java @@ -97,16 +97,17 @@ public class Tabs implements GeckoEventL registerEventListener("Content:ReaderEnabled"); registerEventListener("Content:StateChange"); registerEventListener("Content:LoadError"); registerEventListener("Content:PageShow"); registerEventListener("DOMContentLoaded"); registerEventListener("DOMTitleChanged"); registerEventListener("Link:Favicon"); registerEventListener("Link:Feed"); + registerEventListener("Link:OpenSearch"); registerEventListener("DesktopMode:Changed"); registerEventListener("Tab:ViewportMetadata"); } public synchronized void attachToContext(Context context) { final Context appContext = context.getApplicationContext(); if (mAppContext == appContext) { return; @@ -462,18 +463,21 @@ public class Tabs implements GeckoEventL tab.setErrorType(message.optString("errorType")); notifyListeners(tab, Tabs.TabEvents.LOADED); } else if (event.equals("DOMTitleChanged")) { tab.updateTitle(message.getString("title")); } else if (event.equals("Link:Favicon")) { tab.updateFaviconURL(message.getString("href"), message.getInt("size")); notifyListeners(tab, TabEvents.LINK_FAVICON); } else if (event.equals("Link:Feed")) { - tab.setFeedsEnabled(true); + tab.setHasFeeds(true); notifyListeners(tab, TabEvents.LINK_FEED); + } else if (event.equals("Link:OpenSearch")) { + boolean visible = message.getBoolean("visible"); + tab.setHasOpenSearch(visible); } else if (event.equals("DesktopMode:Changed")) { tab.setDesktopMode(message.getBoolean("desktopMode")); notifyListeners(tab, TabEvents.DESKTOP_MODE_CHANGE); } else if (event.equals("Tab:ViewportMetadata")) { tab.setZoomConstraints(new ZoomConstraints(message)); tab.setIsRTL(message.getBoolean("isRTL")); notifyListeners(tab, TabEvents.VIEWPORT_CHANGE); }
--- a/mobile/android/base/locales/en-US/android_strings.dtd +++ b/mobile/android/base/locales/en-US/android_strings.dtd @@ -223,16 +223,17 @@ size. --> <!ENTITY contextmenu_paste "Paste"> <!ENTITY contextmenu_copyurl "Copy Address"> <!ENTITY contextmenu_edit_bookmark "Edit"> <!ENTITY contextmenu_subscribe "Subscribe to Page"> <!ENTITY contextmenu_site_settings "Edit Site Settings"> <!ENTITY contextmenu_top_sites_edit "Edit"> <!ENTITY contextmenu_top_sites_pin "Pin Site"> <!ENTITY contextmenu_top_sites_unpin "Unpin Site"> +<!ENTITY contextmenu_add_search_engine "Add a Search Engine"> <!ENTITY pref_titlebar_mode "Title bar"> <!ENTITY pref_titlebar_mode_title "Show page title"> <!ENTITY pref_titlebar_mode_url "Show page address"> <!ENTITY history_removed "Page removed"> <!ENTITY bookmark_edit_title "Edit Bookmark">
--- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -143,16 +143,18 @@ gbjar.sources += [ 'GeckoPreferences.java', 'GeckoPreferenceFragment.java', 'GeckoProfile.java', 'GeckoSmsManager.java', 'GeckoThread.java', 'GeckoJavaSampler.java', 'GlobalHistory.java', 'GeckoView.java', + 'GeckoViewChrome.java', + 'GeckoViewContent.java', 'health/BrowserHealthRecorder.java', 'health/BrowserHealthReporter.java', 'InputMethods.java', 'JavaAddonManager.java', 'LightweightTheme.java', 'LightweightThemeDrawable.java', 'LinkPreference.java', 'MemoryMonitor.java',
--- a/mobile/android/base/resources/menu/titlebar_contextmenu.xml +++ b/mobile/android/base/resources/menu/titlebar_contextmenu.xml @@ -12,16 +12,20 @@ android:title="@string/contextmenu_paste"/> <item android:id="@+id/share" android:title="@string/contextmenu_share"/> <item android:id="@+id/subscribe" android:title="@string/contextmenu_subscribe"/> + <item android:id="@+id/add_search_engine" + android:title="@string/contextmenu_add_search_engine" + android:visible="false"/> + <item android:id="@+id/copyurl" android:title="@string/contextmenu_copyurl"/> <item android:id="@+id/site_settings" android:title="@string/contextmenu_site_settings" /> <item android:id="@+id/add_to_launcher" android:title="@string/contextmenu_add_to_launcher"/>
--- a/mobile/android/base/strings.xml.in +++ b/mobile/android/base/strings.xml.in @@ -223,16 +223,17 @@ <string name="contextmenu_paste">&contextmenu_paste;</string> <string name="contextmenu_copyurl">&contextmenu_copyurl;</string> <string name="contextmenu_edit_bookmark">&contextmenu_edit_bookmark;</string> <string name="contextmenu_subscribe">&contextmenu_subscribe;</string> <string name="contextmenu_site_settings">&contextmenu_site_settings;</string> <string name="contextmenu_top_sites_edit">&contextmenu_top_sites_edit;</string> <string name="contextmenu_top_sites_pin">&contextmenu_top_sites_pin;</string> <string name="contextmenu_top_sites_unpin">&contextmenu_top_sites_unpin;</string> + <string name="contextmenu_add_search_engine">&contextmenu_add_search_engine;</string> <string name="pref_titlebar_mode">&pref_titlebar_mode;</string> <string name="pref_titlebar_mode_title">&pref_titlebar_mode_title;</string> <string name="pref_titlebar_mode_url">&pref_titlebar_mode_url;</string> <string name="history_removed">&history_removed;</string> <string name="bookmark_edit_title">&bookmark_edit_title;</string>
--- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -1392,20 +1392,27 @@ var BrowserApp = { params.postData = submission.postData; } } // Don't show progress throbber for about:home or about:reader if (!shouldShowProgress(url)) params.showProgress = false; - if (data.newTab) + if (data.newTab) { this.addTab(url, params); - else + } else { + if (data.tabId) { + // Use a specific browser instead of the selected browser, if it exists + let specificBrowser = this.getTabForId(data.tabId).browser; + if (specificBrowser) + browser = specificBrowser; + } this.loadURI(url, browser, params); + } break; } case "Tab:Selected": this._handleTabSelected(this.getTabForId(parseInt(aData))); break; case "Tab:Closed": @@ -3482,16 +3489,70 @@ Tab.prototype = { this.browser.feeds.push({ href: target.href, title: target.title, type: type }); let json = { type: "Link:Feed", tabID: this.id }; sendMessageToJava(json); } catch (e) {} + } else if (list.indexOf("[search]" != -1)) { + let type = target.type && target.type.toLowerCase(); + + // Replace all starting or trailing spaces or spaces before "*;" globally w/ "". + type = type.replace(/^\s+|\s*(?:;.*)?$/g, ""); + + // Check that type matches opensearch. + let isOpenSearch = (type == "application/opensearchdescription+xml"); + if (isOpenSearch && target.title && /^(?:https?|ftp):/i.test(target.href)) { + let visibleEngines = Services.search.getVisibleEngines(); + // NOTE: Engines are currently identified by name, but this can be changed + // when Engines are identified by URL (see bug 335102). + if (visibleEngines.some(function(e) { + return e.name == target.title; + })) { + // This engine is already present, do nothing. + return; + } + + if (this.browser.engines) { + // This engine has already been handled, do nothing. + if (this.browser.engines.some(function(e) { + return e.url == target.href; + })) { + return; + } + } else { + this.browser.engines = []; + } + + // Get favicon. + let iconURL = target.ownerDocument.documentURIObject.prePath + "/favicon.ico"; + + let newEngine = { + title: target.title, + url: target.href, + iconURL: iconURL + }; + + this.browser.engines.push(newEngine); + + // Don't send a message to display engines if we've already handled an engine. + if (this.browser.engines.length > 1) + return; + + // Broadcast message that this tab contains search engines that should be visible. + let newEngineMessage = { + type: "Link:OpenSearch", + tabID: this.id, + visible: true + }; + + sendMessageToJava(newEngineMessage); + } } break; } case "DOMTitleChanged": { if (!aEvent.isTrusted) return; @@ -3636,16 +3697,32 @@ Tab.prototype = { // Filter optimization: Only really send NETWORK state changes to Java listener if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) { if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) && aWebProgress.isLoadingDocument) { // We may receive a document stop event while a document is still loading // (such as when doing URI fixup). Don't notify Java UI in these cases. return; } + // Clear page-specific opensearch engines and feeds for a new request. + if (aStateFlags & Ci.nsIWebProgressListener.STATE_START && aRequest && aWebProgress.isTopLevel) { + this.browser.engines = null; + + // Send message to clear search engine option in context menu. + let newEngineMessage = { + type: "Link:OpenSearch", + tabID: this.id, + visible: false + }; + + sendMessageToJava(newEngineMessage); + + this.browser.feeds = null; + } + // Check to see if we restoring the content from a previous presentation (session) // since there should be no real network activity let restoring = aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING; let showProgress = restoring ? false : this.showProgress; // true if the page loaded successfully (i.e., no 404s or other errors) let success = false; let uri = ""; @@ -6551,30 +6628,32 @@ OverscrollController.prototype = { }; var SearchEngines = { _contextMenuId: null, PREF_SUGGEST_ENABLED: "browser.search.suggest.enabled", PREF_SUGGEST_PROMPTED: "browser.search.suggest.prompted", init: function init() { + Services.obs.addObserver(this, "SearchEngines:Add", false); Services.obs.addObserver(this, "SearchEngines:Get", false); Services.obs.addObserver(this, "SearchEngines:GetVisible", false); Services.obs.addObserver(this, "SearchEngines:SetDefault", false); Services.obs.addObserver(this, "SearchEngines:Remove", false); let contextName = Strings.browser.GetStringFromName("contextmenu.addSearchEngine"); let filter = { matches: function (aElement) { return (aElement.form && NativeWindow.contextmenus.textContext.matches(aElement)); } }; this._contextMenuId = NativeWindow.contextmenus.add(contextName, filter, this.addEngine); }, uninit: function uninit() { + Services.obs.removeObserver(this, "SearchEngines:Add"); Services.obs.removeObserver(this, "SearchEngines:Get"); Services.obs.removeObserver(this, "SearchEngines:GetVisible"); Services.obs.removeObserver(this, "SearchEngines:SetDefault"); Services.obs.removeObserver(this, "SearchEngines:Remove"); if (this._contextMenuId != null) NativeWindow.contextmenus.remove(this._contextMenuId); }, @@ -6646,16 +6725,19 @@ var SearchEngines = { _extractEngineFromJSON: function _extractEngineFromJSON(aData) { let data = JSON.parse(aData); return Services.search.getEngineByName(data.engine); }, observe: function observe(aSubject, aTopic, aData) { let engine; switch(aTopic) { + case "SearchEngines:Add": + this.displaySearchEnginesList(aData); + break; case "SearchEngines:GetVisible": Services.search.init(this._handleSearchEnginesGetVisible.bind(this)); break; case "SearchEngines:Get": // Return a list of all engines, including "Hidden" ones. Services.search.init(this._handleSearchEnginesGetAll.bind(this)); break; case "SearchEngines:SetDefault": @@ -6672,16 +6754,60 @@ var SearchEngines = { Services.search.removeEngine(engine); break; default: dump("Unexpected message type observed: " + aTopic); break; } }, + // Display context menu listing names of the search engines available to be added. + displaySearchEnginesList: function displaySearchEnginesList(aData) { + let data = JSON.parse(aData); + let tab = BrowserApp.getTabForId(data.tabId); + + if (!tab) + return; + + let browser = tab.browser; + let engines = browser.engines; + + let p = new Prompt({ + window: browser.contentWindow + }).setSingleChoiceItems(engines.map(function(e) { + return { label: e.title }; + })).show((function(data) { + if (data.button == -1) + return; + + this.addOpenSearchEngine(engines[data.button]); + engines.splice(data.button, 1); + + if (engines.length < 1) { + // Broadcast message that there are no more add-able search engines. + let newEngineMessage = { + type: "Link:OpenSearch", + tabID: tab.id, + visible: false + }; + + sendMessageToJava(newEngineMessage); + } + }).bind(this)); + }, + + addOpenSearchEngine: function addOpenSearchEngine(engine) { + Services.search.addEngine(engine.url, Ci.nsISearchEngine.DATA_XML, engine.iconURL, false, { + onSuccess: function() { + // Display a toast confirming addition of new search engine. + NativeWindow.toast.show(Strings.browser.formatStringFromName("alertSearchEngineAddedToast", [engine.title], 1), "long"); + } + }); + }, + addEngine: function addEngine(aElement) { let form = aElement.form; let charset = aElement.ownerDocument.characterSet; let docURI = Services.io.newURI(aElement.ownerDocument.URL, charset, null); let formURL = Services.io.newURI(form.getAttribute("action"), charset, docURI).spec; let method = form.method.toUpperCase(); let formData = []; @@ -7113,16 +7239,17 @@ var RemoteDebugger = { let msg = Strings.browser.GetStringFromName("remoteIncomingPromptMessage"); let disable = Strings.browser.GetStringFromName("remoteIncomingPromptDisable"); let cancel = Strings.browser.GetStringFromName("remoteIncomingPromptCancel"); let agree = Strings.browser.GetStringFromName("remoteIncomingPromptAccept"); // Make prompt. Note: button order is in reverse. let prompt = new Prompt({ window: null, + hint: "remotedebug", title: title, message: msg, buttons: [ agree, cancel, disable ], priority: 1 }); // The debugger server expects a synchronous response, so spin on result since Prompt is async. let result = null;
--- a/mobile/android/components/PromptService.js +++ b/mobile/android/components/PromptService.js @@ -213,28 +213,30 @@ InternalPrompt.prototype = { else return this.nsIAuthPrompt_promptPassword.apply(this, arguments); }, /* ---------- nsIPrompt ---------- */ alert: function alert(aTitle, aText) { let p = this._getPrompt(aTitle, aText, [ PromptUtils.getLocaleString("OK") ]); + p.setHint("alert"); this.showPrompt(p); }, alertCheck: function alertCheck(aTitle, aText, aCheckMsg, aCheckState) { let p = this._getPrompt(aTitle, aText, [ PromptUtils.getLocaleString("OK") ], aCheckMsg, aCheckState); let data = this.showPrompt(p); if (aCheckState && data.button > -1) aCheckState.value = data.checkbox0 == "true"; }, confirm: function confirm(aTitle, aText) { let p = this._getPrompt(aTitle, aText); + p.setHint("confirm"); let data = this.showPrompt(p); return (data.button == 0); }, confirmCheck: function confirmCheck(aTitle, aText, aCheckMsg, aCheckState) { let p = this._getPrompt(aTitle, aText, null, aCheckMsg, aCheckState); let data = this.showPrompt(p); let ok = data.button == 0; @@ -286,16 +288,17 @@ InternalPrompt.prototype = { let data = this.showPrompt(p); if (aCheckState && data.button > -1) aCheckState.value = data.checkbox0 == "true"; return data.button; }, nsIPrompt_prompt: function nsIPrompt_prompt(aTitle, aText, aValue, aCheckMsg, aCheckState) { let p = this._getPrompt(aTitle, aText, null, aCheckMsg, aCheckState); + p.setHint("prompt"); p.addTextbox({ value: aValue.value, autofocus: true }); let data = this.showPrompt(p); let ok = data.button == 0; if (aCheckState && data.button > -1)
--- a/mobile/android/locales/en-US/chrome/browser.properties +++ b/mobile/android/locales/en-US/chrome/browser.properties @@ -17,16 +17,21 @@ alertDownloadsSize=Download too big alertDownloadsNoSpace=Not enough storage space alertDownloadsToast=Download started… alertDownloadsPause=Pause alertDownloadsResume=Resume alertDownloadsCancel=Cancel alertFullScreenToast=Press BACK to leave full-screen mode +# LOCALIZATION NOTE (alertSearchEngineAddedToast) +# %S will be replaced by the name of the search engine (exposed by the current page) +# that has been added; for example, 'Google'. +alertSearchEngineAddedToast='%S' has been added as a search engine + downloadCancelPromptTitle=Cancel Download downloadCancelPromptMessage=Do you want to cancel this download? # LOCALIZATION NOTE (addonError-1, addonError-2, addonError-3, addonError-4): # #1 is the add-on name, #2 is the add-on host, #3 is the application name addonError-1=The add-on could not be downloaded because of a connection failure on #2. addonError-2=The add-on from #2 could not be installed because it does not match the add-on #3 expected. addonError-3=The add-on downloaded from #2 could not be installed because it appears to be corrupt.
--- a/mobile/android/modules/Prompt.jsm +++ b/mobile/android/modules/Prompt.jsm @@ -27,22 +27,33 @@ function Prompt(aOptions) { this.msg.title = aOptions.title; if ("message" in aOptions && aOptions.message != null) this.msg.text = aOptions.message; if ("buttons" in aOptions && aOptions.buttons != null) this.msg.buttons = aOptions.buttons; + if ("hint" in aOptions && aOptions.hint != null) + this.msg.hint = aOptions.hint; + let idService = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); this.guid = idService.generateUUID().toString(); this.msg.guid = this.guid; } Prompt.prototype = { + setHint: function(aHint) { + if (!aHint) + delete this.msg.hint; + else + this.msg.hint = aHint; + return this; + }, + addButton: function(aOptions) { if (!this.msg.buttons) this.msg.buttons = []; this.msg.buttons.push(aOptions.label); return this; }, _addInput: function(aOptions) {
--- a/netwerk/base/public/nsIBrowserSearchService.idl +++ b/netwerk/base/public/nsIBrowserSearchService.idl @@ -17,17 +17,17 @@ interface nsISearchSubmission : nsISuppo readonly attribute nsIInputStream postData; /** * The URI to submit a search to. */ readonly attribute nsIURI uri; }; -[scriptable, uuid(ccf6aa20-10a9-4a0c-a81d-31b10ea846de)] +[scriptable, uuid(7914c4b8-f05b-40c9-a982-38a058cd1769)] interface nsISearchEngine : nsISupports { /** * Gets a nsISearchSubmission object that contains information about what to * send to the search engine, including the URI and postData, if applicable. * * @param data * Data to add to the submission object. @@ -79,16 +79,35 @@ interface nsISearchEngine : nsISupports * given responseType, false otherwise. * * @param responseType * The MIME type to check for */ boolean supportsResponseType(in AString responseType); /** + * Returns a string with the URL to an engine's icon matching both width and + * height. Returns null if icon with specified dimensions is not found. + * + * @param width + * Width of the requested icon. + * @param height + * Height of the requested icon. + */ + AString getIconURLBySize(in long width, in long height); + + /** + * Gets an array of all available icons. Each entry is an object with + * width, height and url properties. width and height are numeric and + * represent the icon's dimensions. url is a string with the URL for + * the icon. + */ + jsval getIcons(); + + /** * Supported search engine types. */ const unsigned long TYPE_MOZSEARCH = 1; const unsigned long TYPE_SHERLOCK = 2; const unsigned long TYPE_OPENSEARCH = 3; /** * Supported search engine data types.
--- a/toolkit/components/search/nsSearchService.js +++ b/toolkit/components/search/nsSearchService.js @@ -1551,47 +1551,90 @@ Engine.prototype = { // Notify the callback if needed if (aEngine._installCallback) { aEngine._installCallback(); } }, /** - * Sets the .iconURI property of the engine. + * Creates a key by serializing an object that contains the icon's width + * and height. + * + * @param aWidth + * Width of the icon. + * @param aHeight + * Height of the icon. + * @returns key string + */ + _getIconKey: function SRCH_ENG_getIconKey(aWidth, aHeight) { + let keyObj = { + width: aWidth, + height: aHeight + }; + + return JSON.stringify(keyObj); + }, + + /** + * Add an icon to the icon map used by getIconURIBySize() and getIcons(). + * + * @param aWidth + * Width of the icon. + * @param aHeight + * Height of the icon. + * @param aURISpec + * String with the icon's URI. + */ + _addIconToMap: function SRCH_ENG_addIconToMap(aWidth, aHeight, aURISpec) { + // Use an object instead of a Map() because it needs to be serializable. + this._iconMapObj = this._iconMapObj || {}; + let key = this._getIconKey(aWidth, aHeight); + this._iconMapObj[key] = aURISpec; + }, + + /** + * Sets the .iconURI property of the engine. If both aWidth and aHeight are + * provided an entry will be added to _iconMapObj that will enable accessing + * icon's data through getIcons() and getIconURIBySize() APIs. * * @param aIconURL * A URI string pointing to the engine's icon. Must have a http[s], * ftp, or data scheme. Icons with HTTP[S] or FTP schemes will be * downloaded and converted to data URIs for storage in the engine * XML files, if the engine is not readonly. * @param aIsPreferred * Whether or not this icon is to be preferred. Preferred icons can * override non-preferred icons. + * @param aWidth (optional) + * Width of the icon. + * @param aHeight (optional) + * Height of the icon. */ - _setIcon: function SRCH_ENG_setIcon(aIconURL, aIsPreferred) { - // If we already have a preferred icon, and this isn't a preferred icon, - // just ignore it. - if (this._hasPreferredIcon && !aIsPreferred) - return; - + _setIcon: function SRCH_ENG_setIcon(aIconURL, aIsPreferred, aWidth, aHeight) { var uri = makeURI(aIconURL); // Ignore bad URIs if (!uri) return; LOG("_setIcon: Setting icon url \"" + limitURILength(uri.spec) + "\" for engine \"" + this.name + "\"."); // Only accept remote icons from http[s] or ftp switch (uri.scheme) { case "data": - this._iconURI = uri; - notifyAction(this, SEARCH_ENGINE_CHANGED); - this._hasPreferredIcon = aIsPreferred; + if (!this._hasPreferredIcon || aIsPreferred) { + this._iconURI = uri; + notifyAction(this, SEARCH_ENGINE_CHANGED); + this._hasPreferredIcon = aIsPreferred; + } + + if (aWidth && aHeight) { + this._addIconToMap(aWidth, aHeight, aIconURL) + } break; case "http": case "https": case "ftp": // No use downloading the icon if the engine file is read-only if (!this._readOnly || getBoolPref(BROWSER_SEARCH_PREF + "cache.enabled", true)) { LOG("_setIcon: Downloading icon: \"" + uri.spec + @@ -1605,17 +1648,22 @@ Engine.prototype = { return; if (!aByteArray || aByteArray.length > MAX_ICON_SIZE) { LOG("iconLoadCallback: load failed, or the icon was too large!"); return; } var str = btoa(String.fromCharCode.apply(null, aByteArray)); - aEngine._iconURI = makeURI(ICON_DATAURL_PREFIX + str); + let dataURL = ICON_DATAURL_PREFIX + str; + aEngine._iconURI = makeURI(dataURL); + + if (aWidth && aHeight) { + aEngine._addIconToMap(aWidth, aHeight, dataURL) + } // The engine might not have a file yet, if it's being downloaded, // because the request for the engine file itself (_onLoad) may not // yet be complete. In that case, this change will be written to // file when _onLoad is called. For readonly engines, we'll store // the changes in the cache once notified below. if (aEngine._file && !aEngine._readOnly) aEngine._serializeToFile(); @@ -1799,24 +1847,27 @@ Engine.prototype = { }, /** * Get the icon from an OpenSearch Image element. * @see http://opensearch.a9.com/spec/1.1/description/#image */ _parseImage: function SRCH_ENG_parseImage(aElement) { LOG("_parseImage: Image textContent: \"" + limitURILength(aElement.textContent) + "\""); - if (aElement.getAttribute("width") == "16" && - aElement.getAttribute("height") == "16") { - this._setIcon(aElement.textContent, true); + + let width = parseInt(aElement.getAttribute("width"), 10); + let height = parseInt(aElement.getAttribute("height"), 10); + let isPrefered = width == 16 && height == 16; + + if (isNaN(width) || isNaN(height) || width <= 0 || height <=0) { + LOG("OpenSearch image element must have positive width and height."); + return; } - else { - LOG("OpenSearch image must have explicit width=16 height=16: " + - aElement.textContent); - } + + this._setIcon(aElement.textContent, isPrefered, width, height); }, _parseAsMozSearch: function SRCH_ENG_parseAsMoz() { //forward to the OpenSearch parser this._parseAsOpenSearch(); }, /** @@ -2204,16 +2255,17 @@ Engine.prototype = { this._updateInterval = aJson._updateInterval || null; this._updateURL = aJson._updateURL || null; this._iconUpdateURL = aJson._iconUpdateURL || null; if (aJson._readOnly == undefined) this._readOnly = true; else this._readOnly = false; this._iconURI = makeURI(aJson._iconURL); + this._iconMapObj = aJson._iconMapObj; for (let i = 0; i < aJson._urls.length; ++i) { let url = aJson._urls[i]; let engineURL = new EngineURL(url.type || URLTYPE_SEARCH_HTML, url.method || "GET", url.template); engineURL._initWithJSON(url, this); this._urls.push(engineURL); } }, @@ -2228,17 +2280,18 @@ Engine.prototype = { _serializeToJSON: function SRCH_ENG__serializeToJSON(aFilter) { var json = { _id: this._id, _name: this._name, _hidden: this.hidden, description: this.description, __searchForm: this.__searchForm, _iconURL: this._iconURL, - _urls: [url._serializeToJSON() for each(url in this._urls)] + _iconMapObj: this._iconMapObj, + _urls: [url._serializeToJSON() for each(url in this._urls)] }; if (this._file instanceof Ci.nsILocalFile) json.filePath = this._file.persistentDescriptor; if (this._uri) json._url = this._uri.spec; if (this._installLocation != SEARCH_APP_DIR || !aFilter) json._installLocation = this._installLocation; @@ -2674,18 +2727,61 @@ Engine.prototype = { if (aIID.equals(Ci.nsISearchEngine) || aIID.equals(Ci.nsISupports)) return this; throw Cr.NS_ERROR_NO_INTERFACE; }, get wrappedJSObject() { return this; + }, + + /** + * Returns a string with the URL to an engine's icon matching both width and + * height. Returns null if icon with specified dimensions is not found. + * + * @param width + * Width of the requested icon. + * @param height + * Height of the requested icon. + */ + getIconURLBySize: function SRCH_ENG_getIconURLBySize(aWidth, aHeight) { + if (!this._iconMapObj) + return null; + + let key = this._getIconKey(aWidth, aHeight); + if (key in this._iconMapObj) { + return this._iconMapObj[key]; + } + return null; + }, + + /** + * Gets an array of all available icons. Each entry is an object with + * width, height and url properties. width and height are numeric and + * represent the icon's dimensions. url is a string with the URL for + * the icon. + */ + getIcons: function SRCH_ENG_getIcons() { + let result = []; + + if (!this._iconMapObj) + return result; + + for (let key of Object.keys(this._iconMapObj)) { + let iconSize = JSON.parse(key); + result.push({ + width: iconSize.width, + height: iconSize.height, + url: this._iconMapObj[key] + }); + } + + return result; } - }; // nsISearchSubmission function Submission(aURI, aPostData = null) { this._uri = aURI; this._postData = aPostData; } Submission.prototype = {
new file mode 100644 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engineImages.xml @@ -0,0 +1,22 @@ +<!-- 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/. --> + +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> + <ShortName>IconsTest</ShortName> + <Description>IconsTest. Search by Test.</Description> + <InputEncoding>UTF-8</InputEncoding> + <Image width="16" height="16">data:image/x-icon;base64,ico16</Image> + <Image width="32" height="32">data:image/x-icon;base64,ico32</Image> + <Image width="74" height="74">data:image/png;base64,ico74</Image> + <Url type="application/x-suggestions+json" template="http://api.bing.com/osjson.aspx"> + <Param name="query" value="{searchTerms}"/> + <Param name="form" value="MOZW"/> + </Url> + <Url type="text/html" method="GET" template="http://www.bing.com/search"> + <Param name="q" value="{searchTerms}"/> + <MozParam name="pc" condition="pref" pref="ms-pc"/> + <Param name="form" value="MOZW"/> + </Url> + <SearchForm>http://www.bing.com/search</SearchForm> +</SearchPlugin>
new file mode 100644 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_multipleIcons.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests getIcons() and getIconURLBySize() on engine with multiple icons. + */ + +"use strict"; + +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://testing-common/httpd.js"); + + function test_multiIcon() { + let engine = Services.search.getEngineByName("IconsTest"); + do_check_neq(engine, null); + + do_print("Running tests on IconsTest engine"); + + do_print("The default should be the 16x16 icon"); + do_check_true(engine.iconURI.spec.contains("ico16")); + + do_check_true(engine.getIconURLBySize(32,32).contains("ico32")); + do_check_true(engine.getIconURLBySize(74,74).contains("ico74")); + + do_print("Invalid dimensions should return null."); + do_check_null(engine.getIconURLBySize(50,50)); + + let allIcons = engine.getIcons(); + + do_print("Check that allIcons contains expected icon sizes"); + do_check_eq(allIcons.length, 3); + let expectedWidths = [16, 32, 74]; + do_check_true(allIcons.every((item) => { + let width = item.width; + do_check_neq(expectedWidths.indexOf(width), -1); + do_check_eq(width, item.height); + + let icon = item.url.split(",").pop(); + do_check_eq(icon, "ico" + width); + + return true; + })); + + do_test_finished(); +} + +function run_test() { + removeMetadata(); + updateAppInfo(); + + let httpServer = new HttpServer(); + httpServer.start(4444); + httpServer.registerDirectory("/", do_get_cwd()); + + do_register_cleanup(function cleanup() { + httpServer.stop(function() {}); + }); + + do_test_pending(); + + let observer = function(aSubject, aTopic, aData) { + if (aData == "engine-loaded") { + test_multiIcon(); + } + }; + + Services.obs.addObserver(observer, "browser-search-engine-modified", false); + + do_print("Adding engine with multiple images"); + Services.search.addEngine("http://localhost:4444/data/engineImages.xml", + Ci.nsISearchEngine.DATA_XML, null, false); + + do_timeout(12000, function() { + do_throw("Timeout"); + }); +}
--- a/toolkit/components/search/tests/xpcshell/xpcshell.ini +++ b/toolkit/components/search/tests/xpcshell/xpcshell.ini @@ -2,16 +2,17 @@ head = head_search.js tail = firefox-appdir = browser support-files = data/chrome.manifest data/engine.src data/engine.xml data/engine2.xml + data/engineImages.xml data/ico-size-16x16-png.ico data/search-metadata.json data/search.json data/search.sqlite data/searchTest.jar [test_nocache.js] [test_645970.js] @@ -22,12 +23,13 @@ support-files = [test_nodb.js] [test_nodb_pluschanges.js] [test_save_sorted_engines.js] [test_purpose.js] [test_defaultEngine.js] [test_prefSync.js] [test_notifications.js] [test_addEngine_callback.js] +[test_multipleIcons.js] [test_async.js] [test_sync.js] [test_sync_fallback.js] [test_sync_delay_fallback.js]
--- a/toolkit/content/widgets/videocontrols.xml +++ b/toolkit/content/widgets/videocontrols.xml @@ -333,17 +333,25 @@ "playing", "waiting", "canplay", "canplaythrough", "seeking", "seeked", "emptied", "loadedmetadata", "error", "suspend", "stalled", "mozinterruptbegin", "mozinterruptend" ], firstFrameShown : false, timeUpdateCount : 0, maxCurrentTimeSeen : 0, - isAudioOnly : false, + _isAudioOnly : false, + get isAudioOnly() { return this._isAudioOnly; }, + set isAudioOnly(val) { + this._isAudioOnly = val; + if (!this.isTopLevelSyntheticDocument) + return; + this.video.style.height = this._controlBarHeight + "px"; + this.video.style.width = "66%"; + }, suppressError : false, setupStatusFader : function(immediate) { // Since the play button will be showing, we don't want to // show the throbber behind it. The throbber here will // only show if needed after the play button has been pressed. if (!this.clickToPlay.hidden) { this.startFadeOut(this.statusOverlay, true); @@ -837,17 +845,20 @@ if (index >= 0) { endTime = Math.round(buffered.end(index) * 1000); } this.bufferBar.max = duration; this.bufferBar.value = endTime; }, onVolumeMouseInOut : function (event) { - if (this.isVideoWithoutAudioTrack()) { + let doc = this.video.ownerDocument; + let win = doc.defaultView; + if (this.isVideoWithoutAudioTrack() || + (this.isAudioOnly && this.isTopLevelSyntheticDocument)) { return; } // Ignore events caused by transitions between mute button and volumeStack, // or between nodes inside these two elements. if (this.isEventWithin(event, this.muteButton, this.volumeStack)) return; var isMouseOver = (event.type == "mouseover"); this.startFade(this.volumeStack, isMouseOver); @@ -1330,44 +1341,52 @@ return isDescendant(event.target) && isDescendant(event.relatedTarget); }, log : function (msg) { if (this.debug) dump("videoctl: " + msg + "\n"); }, + get isTopLevelSyntheticDocument() { + let doc = this.video.ownerDocument; + let win = doc.defaultView; + return doc.mozSyntheticDocument && win === win.top; + }, + _playButtonWidth : 0, _durationLabelWidth : 0, _muteButtonWidth : 0, _fullscreenButtonWidth : 0, _controlBarHeight : 0, _overlayPlayButtonHeight : 64, _overlayPlayButtonWidth : 64, adjustControlSize : function adjustControlSize() { - if (this.isAudioOnly) + let doc = this.video.ownerDocument; + let isAudioOnly = this.isAudioOnly; + if (isAudioOnly && !this.isTopLevelSyntheticDocument) return; - let videoHeight = this.video.clientHeight; - let videoWidth = this.video.clientWidth; - - if (this._overlayPlayButtonHeight > videoHeight || this._overlayPlayButtonWidth > videoWidth) - this.clickToPlay.hidden = true; - // The scrubber has |flex=1|, therefore |minScrubberWidth| // was generated by empirical testing. let minScrubberWidth = 25; let minWidthAllControls = this._playButtonWidth + minScrubberWidth + this._durationLabelWidth + this._muteButtonWidth + this._fullscreenButtonWidth; let minHeightForControlBar = this._controlBarHeight; let minWidthOnlyPlayPause = this._playButtonWidth + this._muteButtonWidth; + let videoHeight = isAudioOnly ? minHeightForControlBar : this.video.clientHeight; + let videoWidth = isAudioOnly ? minWidthAllControls : this.video.clientWidth; + + if (this._overlayPlayButtonHeight > videoHeight || this._overlayPlayButtonWidth > videoWidth) + this.clickToPlay.hidden = true; + let size = "normal"; if (videoHeight < minHeightForControlBar) size = "hidden"; else if (videoWidth < minWidthOnlyPlayPause) size = "hidden"; else if (videoWidth < minWidthAllControls) size = "small"; this.controlBar.setAttribute("size", size);
--- a/toolkit/devtools/output-parser.js +++ b/toolkit/devtools/output-parser.js @@ -3,16 +3,17 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const {Cc, Ci, Cu} = require("chrome"); const {colorUtils} = require("devtools/css-color"); const {Services} = Cu.import("resource://gre/modules/Services.jsm", {}); +const MAX_ITERATIONS = 100; const REGEX_QUOTES = /^".*?"|^".*/; const REGEX_URL = /^url\(["']?(.+?)(?::(\d+))?["']?\)/; const REGEX_WHITESPACE = /^\s+/; const REGEX_FIRST_WORD_OR_CHAR = /^\w+|^./; const REGEX_CSS_PROPERTY_VALUE = /(^[^;]+)/; /** * This regex matches: @@ -130,16 +131,17 @@ OutputParser.prototype = { * parsed. */ _parse: function(text, options={}) { text = text.trim(); this.parsed.length = 0; let dirty = false; let matched = null; let nameValueSupported = false; + let i = 0; let trimMatchFromStart = function(match) { text = text.substr(match.length); dirty = true; matched = null; }; while (text.length > 0) { @@ -202,16 +204,24 @@ OutputParser.prototype = { let match = matched[0]; trimMatchFromStart(match); this._appendTextNode(match); nameValueSupported = false; } } dirty = false; + + // Prevent this loop from slowing down the browser with too + // many nodes being appended into output. + i++; + if (i > MAX_ITERATIONS) { + trimMatchFromStart(text); + this._appendTextNode(text); + } } return this._toDOM(); }, /** * Check if a CSS property supports a specific value. * @@ -307,40 +317,40 @@ OutputParser.prototype = { * Append a text node to the output. If the previously output item was a text * node then we append the text to that node. * * @param {String} text * Text to append */ _appendTextNode: function(text) { let lastItem = this.parsed[this.parsed.length - 1]; - - if (typeof lastItem !== "undefined" && lastItem.nodeName === "#text") { - lastItem.nodeValue += text; + if (typeof lastItem === "string") { + this.parsed[this.parsed.length - 1] = lastItem + text } else { - let win = Services.appShell.hiddenDOMWindow; - let doc = win.document; - let textNode = doc.createTextNode(text); - this.parsed.push(textNode); + this.parsed.push(text); } }, /** * Take all output and append it into a single DocumentFragment. * * @return {DocumentFragment} * Document Fragment */ _toDOM: function() { let win = Services.appShell.hiddenDOMWindow; let doc = win.document; let frag = doc.createDocumentFragment(); for (let item of this.parsed) { - frag.appendChild(item); + if (typeof item === "string") { + frag.appendChild(doc.createTextNode(item)); + } else { + frag.appendChild(item); + } } this.parsed.length = 0; return frag; }, /** * Check that a string represents a valid volor.
--- a/toolkit/modules/RemoteWebProgress.jsm +++ b/toolkit/modules/RemoteWebProgress.jsm @@ -104,16 +104,28 @@ RemoteWebProgressManager.prototype = { // We must check the Extended Validation (EV) state here, on the chrome // process, because NSS is needed for that determination. if (deserialized && deserialized.isExtendedValidation) aState |= Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL; return [deserialized, aState]; }, + _callProgressListeners: function(methodName, ...args) { + for (let p of this._progressListeners) { + if (p[methodName]) { + try { + p[methodName].apply(p, args); + } catch (ex) { + Cu.reportError("RemoteWebProgress failed to call " + methodName + ": " + ex + "\n"); + } + } + } + }, + receiveMessage: function (aMessage) { let json = aMessage.json; let objects = aMessage.objects; // The top-level WebProgress is always the same, but because we don't // really have a concept of subframes/content we always creat a new object // for those. let webProgress = json.isTopLevel ? this._topLevelWebProgress @@ -128,54 +140,46 @@ RemoteWebProgressManager.prototype = { webProgress._DOMWindow = objects.DOMWindow; webProgress._loadType = json.loadType; if (json.isTopLevel) this._browser._contentWindow = objects.contentWindow; switch (aMessage.name) { case "Content:StateChange": - for (let p of this._progressListeners) { - p.onStateChange(webProgress, request, json.stateFlags, json.status); - } + this._callProgressListeners("onStateChange", webProgress, request, json.stateFlags, json.status); break; case "Content:LocationChange": let location = newURI(json.location); if (json.isTopLevel) { this._browser.webNavigation._currentURI = location; this._browser.webNavigation.canGoBack = json.canGoBack; this._browser.webNavigation.canGoForward = json.canGoForward; this._browser._characterSet = json.charset; this._browser._documentURI = newURI(json.documentURI); this._browser._imageDocument = null; } - for (let p of this._progressListeners) { - p.onLocationChange(webProgress, request, location); - } + this._callProgressListeners("onLocationChange", webProgress, request, location); break; case "Content:SecurityChange": let [status, state] = this._fixSSLStatusAndState(json.status, json.state); if (json.isTopLevel) { // Invoking this getter triggers the generation of the underlying object, // which we need to access with ._securityUI, because .securityUI returns // a wrapper that makes _update inaccessible. void this._browser.securityUI; this._browser._securityUI._update(status, state); } - for (let p of this._progressListeners) { - p.onSecurityChange(webProgress, request, state); - } + this._callProgressListeners("onSecurityChange", webProgress, request, state); break; case "Content:StatusChange": - for (let p of this._progressListeners) { - p.onStatusChange(webProgress, request, json.status, json.message); - } + this._callProgressListeners("onStatusChange", webProgress, request, json.status, json.message); break; } } };