Merge m-c to b2g-inbound.

This commit is contained in:
Ryan VanderMeulen 2013-10-30 22:43:14 -04:00
commit 68d25d11d7
25 changed files with 524 additions and 526 deletions

View File

@ -3015,11 +3015,11 @@ const BrowserSearch = {
* allows the search service to provide a different nsISearchSubmission
* depending on e.g. where the search is triggered in the UI.
*
* @return string Name of the search engine used to perform a search or null
* if a search was not performed.
* @return engine The search engine used to perform a search, or null if no
* search was performed.
*/
loadSearch: function BrowserSearch_search(searchText, useNewTab, purpose) {
var engine;
_loadSearch: function (searchText, useNewTab, purpose) {
let engine;
// If the search bar is visible, use the current engine, otherwise, fall
// back to the default engine.
@ -3028,7 +3028,7 @@ const BrowserSearch = {
else
engine = Services.search.defaultEngine;
var submission = engine.getSubmission(searchText, null, purpose); // HTML response
let submission = engine.getSubmission(searchText, null, purpose); // HTML response
// getSubmission can return null if the engine doesn't have a URL
// with a text/html response type. This is unlikely (since
@ -3045,6 +3045,20 @@ const BrowserSearch = {
inBackground: inBackground,
relatedToCurrent: true });
return engine;
},
/**
* Just like _loadSearch, but preserving an old API.
*
* @return string Name of the search engine used to perform a search or null
* if a search was not performed.
*/
loadSearch: function BrowserSearch_search(searchText, useNewTab, purpose) {
let engine = BrowserSearch._loadSearch(searchText, useNewTab, purpose);
if (!engine) {
return null;
}
return engine.name;
},
@ -3055,7 +3069,7 @@ const BrowserSearch = {
* BrowserSearch.loadSearch for the preferred API.
*/
loadSearchFromContext: function (terms) {
let engine = BrowserSearch.loadSearch(terms, true, "contextmenu");
let engine = BrowserSearch._loadSearch(terms, true, "contextmenu");
if (engine) {
BrowserSearch.recordSearchInHealthReport(engine, "contextmenu");
}
@ -3081,8 +3095,7 @@ const BrowserSearch = {
* FHR records only search counts and nothing pertaining to the search itself.
*
* @param engine
* (string) The name of the engine used to perform the search. This
* is typically nsISearchEngine.name.
* (nsISearchEngine) The engine handling the search.
* @param source
* (string) Where the search originated from. See the FHR
* SearchesProvider for allowed values.

View File

@ -486,7 +486,7 @@ function getNumberOfSearches(aEngineName) {
let provider = reporter.getProvider("org.mozilla.searches");
ok(provider, "Searches provider is available.");
let m = provider.getMeasurement("counts", 2);
let m = provider.getMeasurement("counts", 3);
return m.getValues().then(data => {
let now = new Date();
let yday = new Date(now);

View File

@ -47,7 +47,7 @@ function test() {
let provider = reporter.getProvider("org.mozilla.searches");
ok(provider, "Searches provider is available.");
let m = provider.getMeasurement("counts", 2);
let m = provider.getMeasurement("counts", 3);
m.getValues().then(function onValues(data) {
let now = new Date();
ok(data.days.hasDay(now), "Have data for today.");

View File

@ -23,7 +23,7 @@ function test() {
reporter.onInit().then(function onInit() {
let provider = reporter.getProvider("org.mozilla.searches");
ok(provider, "Searches provider is available.");
let m = provider.getMeasurement("counts", 2);
let m = provider.getMeasurement("counts", 3);
m.getValues().then(function onData(data) {
let now = new Date();

View File

@ -320,9 +320,8 @@ BrowserGlue.prototype = {
reporter.onInit().then(function record() {
try {
let name = subject.QueryInterface(Ci.nsISearchEngine).name;
reporter.getProvider("org.mozilla.searches").recordSearch(name,
"urlbar");
let engine = subject.QueryInterface(Ci.nsISearchEngine);
reporter.getProvider("org.mozilla.searches").recordSearch(engine, "urlbar");
} catch (ex) {
Cu.reportError(ex);
}

View File

@ -483,7 +483,7 @@
// null parameter below specifies HTML response for search
var submission = this.currentEngine.getSubmission(aData, null, "searchbar");
BrowserSearch.recordSearchInHealthReport(this.currentEngine.name, "searchbar");
BrowserSearch.recordSearchInHealthReport(this.currentEngine, "searchbar");
openUILinkIn(submission.uri.spec, aWhere, null, submission.postData);
]]></body>
</method>

View File

@ -27,14 +27,15 @@ function test() {
ok(reporter, "Health Reporter available.");
reporter.onInit().then(function onInit() {
let provider = reporter.getProvider("org.mozilla.searches");
let m = provider.getMeasurement("counts", 2);
let m = provider.getMeasurement("counts", 3);
m.getValues().then(function onData(data) {
let now = new Date();
let oldCount = 0;
// Foo engine goes into "other" bucket.
let field = "other.searchbar";
// Find the right bucket for the "Foo" engine.
let engine = Services.search.getEngineByName("Foo");
let field = (engine.identifier || "other-Foo") + ".searchbar";
if (data.days.hasDay(now)) {
let day = data.days.getDay(now);

View File

@ -306,7 +306,20 @@ TabTarget.prototype = {
this._client.connect((aType, aTraits) => {
this._client.listTabs(aResponse => {
this._root = aResponse;
this._form = aResponse.tabs[aResponse.selected];
let windowUtils = this.window
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
let outerWindow = windowUtils.outerWindowID;
aResponse.tabs.some((tab) => {
if (tab.outerWindowID === outerWindow) {
this._form = tab;
return true;
}
});
if (!this._form) {
this._form = aResponse.tabs[aResponse.selected];
}
attachTab();
});
});

View File

@ -165,11 +165,12 @@ let AboutHome = {
Cu.reportError(ex);
break;
}
let engine = Services.search.currentEngine;
#ifdef MOZ_SERVICES_HEALTHREPORT
window.BrowserSearch.recordSearchInHealthReport(data.engineName, "abouthome");
window.BrowserSearch.recordSearchInHealthReport(engine, "abouthome");
#endif
// Trigger a search through nsISearchEngine.getSubmission()
let submission = Services.search.currentEngine.getSubmission(data.searchTerms, null, "homepage");
let submission = engine.getSubmission(data.searchTerms, null, "homepage");
window.loadURI(submission.uri.spec, null, submission.postData);
break;
}

View File

@ -643,8 +643,7 @@ static void RecordFrameMetrics(nsIFrame* aForFrame,
nsRect* aDisplayPort,
nsRect* aCriticalDisplayPort,
ViewID aScrollId,
const nsDisplayItem::ContainerParameters& aContainerParameters,
bool aMayHaveTouchListeners) {
const nsDisplayItem::ContainerParameters& aContainerParameters) {
nsPresContext* presContext = aForFrame->PresContext();
int32_t auPerDevPixel = presContext->AppUnitsPerDevPixel();
LayoutDeviceToLayerScale resolution(aContainerParameters.mXScale, aContainerParameters.mYScale);
@ -719,7 +718,16 @@ static void RecordFrameMetrics(nsIFrame* aForFrame,
metrics.mZoom = metrics.mCumulativeResolution * metrics.mDevPixelsPerCSSPixel
* layerToScreenScale;
metrics.mMayHaveTouchListeners = aMayHaveTouchListeners;
if (presShell) {
nsIDocument* document = nullptr;
document = presShell->GetDocument();
if (document) {
nsCOMPtr<nsPIDOMWindow> innerWin(document->GetInnerWindow());
if (innerWin) {
metrics.mMayHaveTouchListeners = innerWin->HasTouchEventListeners();
}
}
}
// Calculate the composition bounds as the size of the scroll frame and
// its origin relative to the reference frame.
@ -1248,14 +1256,6 @@ void nsDisplayList::PaintForFrame(nsDisplayListBuilder* aBuilder,
}
}
bool mayHaveTouchListeners = false;
if (document) {
nsCOMPtr<nsPIDOMWindow> innerWin(document->GetInnerWindow());
if (innerWin) {
mayHaveTouchListeners = innerWin->HasTouchEventListeners();
}
}
nsRect viewport(aBuilder->ToReferenceFrame(aForFrame), aForFrame->GetSize());
RecordFrameMetrics(aForFrame, rootScrollFrame,
@ -1263,7 +1263,7 @@ void nsDisplayList::PaintForFrame(nsDisplayListBuilder* aBuilder,
root, mVisibleRect, viewport,
(usingDisplayport ? &displayport : nullptr),
(usingCriticalDisplayport ? &criticalDisplayport : nullptr),
id, containerParameters, mayHaveTouchListeners);
id, containerParameters);
if (usingDisplayport &&
!(root->GetContentFlags() & Layer::CONTENT_OPAQUE)) {
// See bug 693938, attachment 567017
@ -3576,7 +3576,7 @@ nsDisplayScrollLayer::BuildLayer(nsDisplayListBuilder* aBuilder,
mVisibleRect, viewport,
(usingDisplayport ? &displayport : nullptr),
(usingCriticalDisplayport ? &criticalDisplayport : nullptr),
scrollId, aContainerParameters, false);
scrollId, aContainerParameters);
return layer.forget();
}

View File

@ -19,6 +19,7 @@ import org.mozilla.gecko.health.BrowserHealthReporter;
import org.mozilla.gecko.home.BrowserSearch;
import org.mozilla.gecko.home.HomePager;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import org.mozilla.gecko.home.SearchEngine;
import org.mozilla.gecko.menu.GeckoMenu;
import org.mozilla.gecko.prompts.Prompt;
import org.mozilla.gecko.util.Clipboard;
@ -167,6 +168,15 @@ abstract public class BrowserApp extends GeckoApp
private BrowserHealthReporter mBrowserHealthReporter;
// The tab to be selected on editing mode exit.
private Integer mTargetTabForEditingMode = null;
// The animator used to toggle HomePager visibility has a race where if the HomePager is shown
// (starting the animation), the HomePager is hidden, and the HomePager animation completes,
// both the web content and the HomePager will be hidden. This flag is used to prevent the
// race by determining if the web content should be hidden at the animation's end.
private boolean mHideWebContentOnAnimationEnd = false;
private SiteIdentityPopup mSiteIdentityPopup;
public SiteIdentityPopup getSiteIdentityPopup() {
@ -481,7 +491,11 @@ abstract public class BrowserApp extends GeckoApp
mBrowserToolbar.setOnStopEditingListener(new BrowserToolbar.OnStopEditingListener() {
public void onStopEditing() {
// Re-enable doorhanger notifications.
selectTargetTabForEditingMode();
hideHomePager();
hideBrowserSearch();
// Re-enable doorhanger notifications. They may trigger on the selected tab above.
mDoorHangerPopup.enable();
}
});
@ -1301,32 +1315,29 @@ abstract public class BrowserApp extends GeckoApp
return false;
}
// If this tab is already selected, just hide the home pager.
if (tabs.isSelectedTabId(tabId)) {
hideHomePager();
} else {
tabs.selectTab(tabId);
}
// Set the target tab to null so it does not get selected (on editing
// mode exit) in lieu of the tab we are about to select.
mTargetTabForEditingMode = null;
Tabs.getInstance().selectTab(tabId);
hideBrowserSearch();
mBrowserToolbar.cancelEdit();
return true;
}
private void openUrl(String url) {
openUrl(url, null, false);
private void openUrlAndStopEditing(String url) {
openUrlAndStopEditing(url, null, false);
}
private void openUrl(String url, boolean newTab) {
openUrl(url, null, newTab);
private void openUrlAndStopEditing(String url, boolean newTab) {
openUrlAndStopEditing(url, null, newTab);
}
private void openUrl(String url, String searchEngine) {
openUrl(url, searchEngine, false);
private void openUrlAndStopEditing(String url, String searchEngine) {
openUrlAndStopEditing(url, searchEngine, false);
}
private void openUrl(String url, String searchEngine, boolean newTab) {
private void openUrlAndStopEditing(String url, String searchEngine, boolean newTab) {
mBrowserToolbar.setProgressVisibility(true);
int flags = Tabs.LOADURL_NONE;
@ -1336,7 +1347,6 @@ abstract public class BrowserApp extends GeckoApp
Tabs.getInstance().loadUrl(url, searchEngine, -1, flags);
hideBrowserSearch();
mBrowserToolbar.cancelEdit();
}
@ -1419,6 +1429,9 @@ abstract public class BrowserApp extends GeckoApp
throw new IllegalArgumentException("Cannot handle null URLs in enterEditingMode");
}
final Tab selectedTab = Tabs.getInstance().getSelectedTab();
mTargetTabForEditingMode = (selectedTab != null ? selectedTab.getId() : null);
final PropertyAnimator animator = new PropertyAnimator(250);
animator.setUseHardwareLayer(false);
@ -1434,8 +1447,6 @@ abstract public class BrowserApp extends GeckoApp
}
final String url = mBrowserToolbar.commitEdit();
hideHomePager();
hideBrowserSearch();
// Don't do anything if the user entered an empty URL.
if (TextUtils.isEmpty(url)) {
@ -1485,15 +1496,18 @@ abstract public class BrowserApp extends GeckoApp
/**
* Record in Health Report that a search has occurred.
*
* @param identifier
* a search identifier, such as "partnername". Can be null.
* @param engine
* a search engine instance. Can be null.
* @param where
* where the search was initialized; one of the values in
* {@link BrowserHealthRecorder#SEARCH_LOCATIONS}.
*/
private static void recordSearch(String identifier, String where) {
Log.i(LOGTAG, "Recording search: " + identifier + ", " + where);
private static void recordSearch(SearchEngine engine, String where) {
Log.i(LOGTAG, "Recording search: " +
((engine == null) ? "null" : engine.name) +
", " + where);
try {
String identifier = (engine == null) ? "other" : engine.getEngineIdentifier();
JSONObject message = new JSONObject();
message.put("type", BrowserHealthRecorder.EVENT_SEARCH);
message.put("location", where);
@ -1509,13 +1523,13 @@ abstract public class BrowserApp extends GeckoApp
return false;
}
mBrowserToolbar.cancelEdit();
// Resetting the visibility of HomePager, which might have been hidden
// by the filterEditingMode().
// cancelEdit will call hideHomePager. If we're on web content, this is fine. If we're on
// about:home, the HomePager needs to be visible in the end (note that hideHomePager will
// not hide the HomePager on about:home). However, filterEditingMode may have hidden the
// HomePager so we set it visible here.
mHomePager.setVisibility(View.VISIBLE);
hideHomePager();
hideBrowserSearch();
mBrowserToolbar.cancelEdit();
return true;
}
@ -1531,6 +1545,22 @@ abstract public class BrowserApp extends GeckoApp
}
}
/**
* Selects the target tab for editing mode. This is expected to be the tab selected on editing
* mode entry, unless it is subsequently overridden.
*
* A background tab may be selected while editing mode is active (e.g. popups), causing the
* new url to load in the newly selected tab. Call this method on editing mode exit to
* mitigate this.
*/
private void selectTargetTabForEditingMode() {
if (mTargetTabForEditingMode != null) {
Tabs.getInstance().selectTab(mTargetTabForEditingMode);
}
mTargetTabForEditingMode = null;
}
/**
* Shows or hides the home pager for the given tab.
*/
@ -1574,7 +1604,38 @@ abstract public class BrowserApp extends GeckoApp
final ViewStub homePagerStub = (ViewStub) findViewById(R.id.home_pager_stub);
mHomePager = (HomePager) homePagerStub.inflate();
}
mHomePager.show(getSupportFragmentManager(), page, animator);
// Hide the web content so it cannot be focused by screen readers.
hideWebContentOnPropertyAnimationEnd(animator);
}
private void hideWebContentOnPropertyAnimationEnd(final PropertyAnimator animator) {
if (animator == null) {
hideWebContent();
return;
}
animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
@Override
public void onPropertyAnimationStart() {
mHideWebContentOnAnimationEnd = true;
}
@Override
public void onPropertyAnimationEnd() {
if (mHideWebContentOnAnimationEnd) {
hideWebContent();
}
}
});
}
private void hideWebContent() {
// The view is set to INVISIBLE, rather than GONE, to avoid
// the additional requestLayout() call.
mLayerView.setVisibility(View.INVISIBLE);
}
private void hideHomePager() {
@ -1587,6 +1648,12 @@ abstract public class BrowserApp extends GeckoApp
return;
}
// Prevent race in hiding web content - see declaration for more info.
mHideWebContentOnAnimationEnd = false;
// Display the previously hidden web content (which prevented screen reader access).
mLayerView.setVisibility(View.VISIBLE);
if (mHomePager != null) {
mHomePager.hide();
}
@ -2293,7 +2360,7 @@ abstract public class BrowserApp extends GeckoApp
for (String url : urls) {
if (!maybeSwitchToTab(url, flags)) {
openUrl(url, true);
openUrlAndStopEditing(url, true);
}
}
}
@ -2302,15 +2369,15 @@ abstract public class BrowserApp extends GeckoApp
@Override
public void onUrlOpen(String url, EnumSet<OnUrlOpenListener.Flags> flags) {
if (!maybeSwitchToTab(url, flags)) {
openUrl(url);
openUrlAndStopEditing(url);
}
}
// BrowserSearch.OnSearchListener
@Override
public void onSearch(String engineId, String text) {
recordSearch(engineId, "barsuggest");
openUrl(text, engineId);
public void onSearch(SearchEngine engine, String text) {
recordSearch(engine, "barsuggest");
openUrlAndStopEditing(text, engine.name);
}
// BrowserSearch.OnEditSuggestionListener

View File

@ -410,6 +410,7 @@ public class BrowserToolbar extends GeckoRelativeLayout
}
if (keyCode == KeyEvent.KEYCODE_BACK) {
// Drop the virtual keyboard.
clearFocus();
return true;
}

View File

@ -61,6 +61,11 @@ public final class BitmapUtils {
return GeckoJarReader.getBitmapDrawable(context.getResources(), data);
}
// Don't attempt to validate the JAR signature when loading an add-on icon
if (data.startsWith("jar:file")) {
return GeckoJarReader.getBitmapDrawable(context.getResources(), Uri.decode(data));
}
URL url = new URL(data);
InputStream is = (InputStream) url.getContent();
try {

View File

@ -741,7 +741,7 @@ public class BrowserHealthRecorder implements GeckoEventListener {
*/
public static final String MEASUREMENT_NAME_SEARCH_COUNTS = "org.mozilla.searches.counts";
public static final int MEASUREMENT_VERSION_SEARCH_COUNTS = 4;
public static final int MEASUREMENT_VERSION_SEARCH_COUNTS = 5;
public static final String[] SEARCH_LOCATIONS = {
"barkeyword",
@ -749,84 +749,6 @@ public class BrowserHealthRecorder implements GeckoEventListener {
"bartext",
};
// See services/healthreport/providers.jsm. Sorry for the duplication.
// THIS LIST MUST BE SORTED per java.lang.Comparable<String>.
private static final String[] SEARCH_PROVIDERS = {
"amazon-co-uk",
"amazon-de",
"amazon-en-GB",
"amazon-france",
"amazon-it",
"amazon-jp",
"amazondotcn",
"amazondotcom",
"amazondotcom-de",
"aol-en-GB",
"aol-web-search",
"bing",
"eBay",
"eBay-de",
"eBay-en-GB",
"eBay-es",
"eBay-fi",
"eBay-france",
"eBay-hu",
"eBay-in",
"eBay-it",
"google",
"google-jp",
"google-ku",
"google-maps-zh-TW",
"mailru",
"mercadolibre-ar",
"mercadolibre-cl",
"mercadolibre-mx",
"seznam-cz",
"twitter",
"twitter-de",
"twitter-ja",
"wikipedia", // Manually added.
"yahoo",
"yahoo-NO",
"yahoo-answer-zh-TW",
"yahoo-ar",
"yahoo-bid-zh-TW",
"yahoo-br",
"yahoo-ch",
"yahoo-cl",
"yahoo-de",
"yahoo-en-GB",
"yahoo-es",
"yahoo-fi",
"yahoo-france",
"yahoo-fy-NL",
"yahoo-id",
"yahoo-in",
"yahoo-it",
"yahoo-jp",
"yahoo-jp-auctions",
"yahoo-mx",
"yahoo-sv-SE",
"yahoo-zh-TW",
"yandex",
"yandex-ru",
"yandex-slovari",
"yandex-tr",
"yandex.by",
"yandex.ru-be",
};
private void initializeSearchProvider() {
this.storage.ensureMeasurementInitialized(
MEASUREMENT_NAME_SEARCH_COUNTS,
@ -854,30 +776,13 @@ public class BrowserHealthRecorder implements GeckoEventListener {
this.dispatcher.registerEventListener(EVENT_SEARCH, this);
}
/**
* Return the field key for the search provider. This turns null and
* non-partner providers into "other".
*
* @param engine an engine identifier, such as "yandex"
* @return the key to use, such as "other" or "yandex".
*/
protected String getEngineKey(final String engine) {
if (engine == null) {
return "other";
}
// This is inefficient. Optimize if necessary.
boolean found = (0 <= java.util.Arrays.binarySearch(SEARCH_PROVIDERS, engine));
return found ? engine : "other";
}
/**
* Record a search.
*
* @param engine the string identifier for the engine, or null if it's not a partner.
* @param engineID the string identifier for the engine. Can be <code>null</code>.
* @param location one of a fixed set of locations: see {@link #SEARCH_LOCATIONS}.
*/
public void recordSearch(final String engine, final String location) {
public void recordSearch(final String engineID, final String location) {
if (this.state != State.INITIALIZED) {
Log.d(LOG_TAG, "Not initialized: not recording search. (" + this.state + ")");
return;
@ -889,7 +794,7 @@ public class BrowserHealthRecorder implements GeckoEventListener {
final int day = storage.getDay();
final int env = this.env;
final String key = getEngineKey(engine);
final String key = (engineID == null) ? "other" : engineID;
final BrowserHealthRecorder self = this;
ThreadUtils.postToBackgroundThread(new Runnable() {

View File

@ -13,8 +13,8 @@ import org.mozilla.gecko.R;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.db.BrowserDB.URLColumns;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import org.mozilla.gecko.home.SearchEngine;
import org.mozilla.gecko.home.SearchLoader.SearchCursorLoader;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.StringUtils;
@ -132,7 +132,7 @@ public class BrowserSearch extends HomeFragment
private View mSuggestionsOptInPrompt;
public interface OnSearchListener {
public void onSearch(String engineId, String text);
public void onSearch(SearchEngine engine, String text);
}
public interface OnEditSuggestionListener {
@ -393,7 +393,7 @@ public class BrowserSearch extends HomeFragment
}
private void setSuggestions(ArrayList<String> suggestions) {
mSearchEngines.get(0).suggestions = suggestions;
mSearchEngines.get(0).setSuggestions(suggestions);
mAdapter.notifyDataSetChanged();
}
@ -417,14 +417,11 @@ public class BrowserSearch extends HomeFragment
ArrayList<SearchEngine> searchEngines = new ArrayList<SearchEngine>();
for (int i = 0; i < engines.length(); i++) {
final JSONObject engineJSON = engines.getJSONObject(i);
final String name = engineJSON.getString("name");
final String identifier = engineJSON.getString("identifier");
final String iconURI = engineJSON.getString("iconURI");
final Bitmap icon = BitmapUtils.getBitmapFromDataURI(iconURI);
final SearchEngine engine = new SearchEngine(engineJSON);
if (name.equals(suggestEngine) && suggestTemplate != null) {
// Suggest engine should be at the front of the list
searchEngines.add(0, new SearchEngine(name, identifier, icon));
if (engine.name.equals(suggestEngine) && suggestTemplate != null) {
// Suggest engine should be at the front of the list.
searchEngines.add(0, engine);
// The only time Tabs.getInstance().getSelectedTab() should
// be null is when we're restoring after a crash. We should
@ -441,7 +438,7 @@ public class BrowserSearch extends HomeFragment
SUGGESTION_TIMEOUT, SUGGESTION_MAX);
}
} else {
searchEngines.add(new SearchEngine(name, identifier, icon));
searchEngines.add(engine);
}
}
@ -713,7 +710,7 @@ public class BrowserSearch extends HomeFragment
// row contains multiple items, clicking the row will do nothing.
final int index = getEngineIndex(position);
if (index != -1) {
return mSearchEngines.get(index).suggestions.isEmpty();
return !mSearchEngines.get(index).hasSuggestions();
}
return true;
@ -744,7 +741,7 @@ public class BrowserSearch extends HomeFragment
row.setSearchTerm(mSearchTerm);
final SearchEngine engine = mSearchEngines.get(getEngineIndex(position));
final boolean animate = (mAnimateSuggestions && engine.suggestions.size() > 0);
final boolean animate = (mAnimateSuggestions && engine.hasSuggestions());
row.updateFromSearchEngine(engine, animate);
if (animate) {
// Only animate suggestions the first time they are shown
@ -877,13 +874,13 @@ public class BrowserSearch extends HomeFragment
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
public boolean onTouchEvent(MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
// Dismiss the soft keyboard.
requestFocus();
}
return super.onInterceptTouchEvent(event);
return super.onTouchEvent(event);
}
}
}

View File

@ -96,6 +96,13 @@ public class HomePager extends ViewPager {
// This is to keep all 4 pages in memory after they are
// selected in the pager.
setOffscreenPageLimit(3);
// We can call HomePager.requestFocus to steal focus from the URL bar and drop the soft
// keyboard. However, if there are no focusable views (e.g. an empty reading list), the
// URL bar will be refocused. Therefore, we make the HomePager container focusable to
// ensure there is always a focusable view. This would ordinarily be done via an XML
// attribute, but it is not working properly.
setFocusableInTouchMode(true);
}
@Override
@ -324,10 +331,7 @@ public class HomePager extends ViewPager {
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
// XXX: Drop the soft keyboard by stealing focus. Note that the HomePager (via XML
// attr) is focusable after its descendants allowing requestFocus to succeed and drop
// the soft keyboard even if there are no other focusable views on the screen (e.g.
// the Reading List is empty).
// Drop the soft keyboard by stealing focus from the URL bar.
requestFocus();
}

View File

@ -5,25 +5,89 @@
package org.mozilla.gecko.home;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.json.JSONException;
import org.json.JSONObject;
import android.graphics.Bitmap;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
class SearchEngine {
public String name;
public String identifier;
public Bitmap icon;
public ArrayList<String> suggestions;
public class SearchEngine {
public static final String LOG_TAG = "GeckoSearchEngine";
public SearchEngine(String name, String identifier) {
this(name, identifier, null);
public final String name; // Never null.
public final String identifier; // Can be null.
private final Bitmap icon;
private volatile List<String> suggestions = new ArrayList<String>(); // Never null.
public SearchEngine(JSONObject engineJSON) throws JSONException {
if (engineJSON == null) {
throw new IllegalArgumentException("Can't instantiate SearchEngine from null JSON.");
}
this.name = getString(engineJSON, "name");
if (this.name == null) {
throw new IllegalArgumentException("Cannot have an unnamed search engine.");
}
this.identifier = getString(engineJSON, "identifier");
final String iconURI = getString(engineJSON, "iconURI");
if (iconURI == null) {
Log.w(LOG_TAG, "iconURI is null for search engine " + this.name);
this.icon = null;
return;
}
this.icon = BitmapUtils.getBitmapFromDataURI(iconURI);
}
public SearchEngine(String name, String identifier, Bitmap icon) {
this.name = name;
this.identifier = identifier;
this.icon = icon;
this.suggestions = new ArrayList<String>();
private static String getString(JSONObject data, String key) throws JSONException {
if (data.isNull(key)) {
return null;
}
return data.getString(key);
}
/**
* @return a non-null string suitable for use by FHR.
*/
public String getEngineIdentifier() {
if (this.identifier != null) {
return this.identifier;
}
if (this.name != null) {
return "other-" + this.name;
}
return "other";
}
public boolean hasSuggestions() {
return !this.suggestions.isEmpty();
}
public int getSuggestionsCount() {
return this.suggestions.size();
}
public Iterable<String> getSuggestions() {
return this.suggestions;
}
public void setSuggestions(List<String> suggestions) {
if (suggestions == null) {
this.suggestions = new ArrayList<String>();
return;
}
this.suggestions = suggestions;
}
public Bitmap getIcon() {
return this.icon;
}
}

View File

@ -82,7 +82,7 @@ class SearchEngineRow extends AnimatedHeightLayout {
mUrlOpenListener.onUrlOpen(suggestion, EnumSet.noneOf(OnUrlOpenListener.Flags.class));
}
} else if (mSearchListener != null) {
mSearchListener.onSearch(mSearchEngine.name, suggestion);
mSearchListener.onSearch(mSearchEngine, suggestion);
}
}
};
@ -135,7 +135,7 @@ class SearchEngineRow extends AnimatedHeightLayout {
public void performUserEnteredSearch() {
String searchTerm = getSuggestionTextFromView(mUserEnteredView);
if (mSearchListener != null) {
mSearchListener.onSearch(mSearchEngine.name, searchTerm);
mSearchListener.onSearch(mSearchEngine, searchTerm);
}
}
@ -162,25 +162,25 @@ class SearchEngineRow extends AnimatedHeightLayout {
}
public void updateFromSearchEngine(SearchEngine searchEngine, boolean animate) {
// Update search engine reference
// Update search engine reference.
mSearchEngine = searchEngine;
// Set the search engine icon (e.g., Google) for the row
mIconView.updateAndScaleImage(mSearchEngine.icon, mSearchEngine.name);
// Set the search engine icon (e.g., Google) for the row.
mIconView.updateAndScaleImage(mSearchEngine.getIcon(), mSearchEngine.getEngineIdentifier());
// Set the initial content description
// Set the initial content description.
setDescriptionOnSuggestion(mUserEnteredTextView, mUserEnteredTextView.getText().toString());
// Add additional suggestions given by this engine
// Add additional suggestions given by this engine.
final int recycledSuggestionCount = mSuggestionView.getChildCount();
final int suggestionCount = mSearchEngine.suggestions.size();
for (int i = 0; i < suggestionCount; i++) {
int suggestionCounter = 0;
for (String suggestion : mSearchEngine.getSuggestions()) {
final View suggestionItem;
// Reuse suggestion views from recycled view, if possible
if (i + 1 < recycledSuggestionCount) {
suggestionItem = mSuggestionView.getChildAt(i + 1);
// Reuse suggestion views from recycled view, if possible.
if (suggestionCounter + 1 < recycledSuggestionCount) {
suggestionItem = mSuggestionView.getChildAt(suggestionCounter + 1);
suggestionItem.setVisibility(View.VISIBLE);
} else {
suggestionItem = mInflater.inflate(R.layout.suggestion_item, null);
@ -195,23 +195,24 @@ class SearchEngineRow extends AnimatedHeightLayout {
mSuggestionView.addView(suggestionItem);
}
final String suggestion = mSearchEngine.suggestions.get(i);
setSuggestionOnView(suggestionItem, suggestion);
if (animate) {
AlphaAnimation anim = new AlphaAnimation(0, 1);
anim.setDuration(ANIMATION_DURATION);
anim.setStartOffset(i * ANIMATION_DURATION);
anim.setStartOffset(suggestionCounter * ANIMATION_DURATION);
suggestionItem.startAnimation(anim);
}
++suggestionCounter;
}
// Hide extra suggestions that have been recycled
for (int i = suggestionCount + 1; i < recycledSuggestionCount; i++) {
// Hide extra suggestions that have been recycled.
for (int i = suggestionCounter + 1; i < recycledSuggestionCount; ++i) {
mSuggestionView.getChildAt(i).setVisibility(View.GONE);
}
// Make sure mSelectedView is still valid
// Make sure mSelectedView is still valid.
if (mSelectedView >= mSuggestionView.getChildCount()) {
mSelectedView = mSuggestionView.getChildCount() - 1;
}

View File

@ -55,20 +55,25 @@
android:layout_alignParentBottom="true">
</RelativeLayout>
<org.mozilla.gecko.BrowserToolbar android:id="@+id/browser_toolbar"
<FrameLayout android:id="@+id/search_container"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_below="@+id/browser_toolbar"
android:background="@android:color/white"
android:visibility="invisible"/>
<!-- When focus is cleared from from BrowserToolbar's EditText to
lower the virtual keyboard, focus will be returned to the root
view. To make sure the EditText is not the first focusable view in
the root view, BrowserToolbar should be specified as low in the
view hierarchy as possible. -->
<org.mozilla.gecko.BrowserToolbar android:id="@id/browser_toolbar"
style="@style/BrowserToolbar"
android:layout_width="fill_parent"
android:layout_height="@dimen/browser_toolbar_height"
android:clickable="true"
android:focusable="true"/>
<FrameLayout android:id="@+id/search_container"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_below="@id/browser_toolbar"
android:background="@android:color/white"
android:visibility="invisible"/>
</view>
<LinearLayout android:id="@+id/toast"

View File

@ -1186,71 +1186,20 @@ SearchCountMeasurement1.prototype = Object.freeze({
* We don't use the search engine name directly, because it is shared across
* locales; e.g., eBay-de and eBay both share the name "eBay".
*/
function SearchCountMeasurement2() {
this._fieldSpecs = null;
this._interestingEngines = null; // Name -> ID. ("Amazon.com" -> "amazondotcom")
function SearchCountMeasurementBase() {
this._fieldSpecs = {};
Metrics.Measurement.call(this);
}
SearchCountMeasurement2.prototype = Object.freeze({
SearchCountMeasurementBase.prototype = Object.freeze({
__proto__: Metrics.Measurement.prototype,
name: "counts",
version: 2,
/**
* Default implementation; can be overridden by test helpers.
*/
getDefaultEngines: function () {
return Services.search.getDefaultEngines();
},
_initialize: function () {
// Don't create all of these for every profile.
// There are 61 partner engines, translating to 244 fields.
// Instead, compute only those that are possible -- those for whom the
// provider is one of the default search engines.
// This set can grow over time, and change as users run different localized
// Firefox instances.
this._fieldSpecs = {};
this._interestingEngines = {};
for (let source of this.SOURCES) {
this._fieldSpecs["other." + source] = DAILY_COUNTER_FIELD;
}
let engines = this.getDefaultEngines();
for (let engine of engines) {
let id = engine.identifier;
if (!id || (this.PROVIDERS.indexOf(id) == -1)) {
continue;
}
this._interestingEngines[engine.name] = id;
let fieldPrefix = id + ".";
for (let source of this.SOURCES) {
this._fieldSpecs[fieldPrefix + source] = DAILY_COUNTER_FIELD;
}
}
},
// Our fields are dynamic, so we compute them into _fieldSpecs by looking at
// the current set of interesting engines.
// Our fields are dynamic.
get fields() {
if (!this._fieldSpecs) {
this._initialize();
}
return this._fieldSpecs;
},
get interestingEngines() {
if (!this._fieldSpecs) {
this._initialize();
}
return this._interestingEngines;
},
/**
* Override the default behavior: serializers should include every counter
* field from the DB, even if we don't currently have it registered.
@ -1280,101 +1229,6 @@ SearchCountMeasurement2.prototype = Object.freeze({
return Metrics.Storage.FIELD_DAILY_COUNTER;
},
// You can compute the total list of fields by unifying the entire l10n repo
// set with the list of partners:
//
// sort -u */*/searchplugins/list.txt | tr -d '^M' | uniq | grep -f partners.txt
//
// where partners.txt contains
//
// amazon
// aol
// bing
// eBay
// google
// mailru
// mercadolibre
// seznam
// twitter
// yahoo
// yandex
//
// Please update this list as the set of partners changes.
//
PROVIDERS: [
"amazon-co-uk",
"amazon-de",
"amazon-en-GB",
"amazon-france",
"amazon-it",
"amazon-jp",
"amazondotcn",
"amazondotcom",
"amazondotcom-de",
"aol-en-GB",
"aol-web-search",
"bing",
"eBay",
"eBay-de",
"eBay-en-GB",
"eBay-es",
"eBay-fi",
"eBay-france",
"eBay-hu",
"eBay-in",
"eBay-it",
"google",
"google-jp",
"google-ku",
"google-maps-zh-TW",
"mailru",
"mercadolibre-ar",
"mercadolibre-cl",
"mercadolibre-mx",
"seznam-cz",
"twitter",
"twitter-de",
"twitter-ja",
"yahoo",
"yahoo-NO",
"yahoo-answer-zh-TW",
"yahoo-ar",
"yahoo-bid-zh-TW",
"yahoo-br",
"yahoo-ch",
"yahoo-cl",
"yahoo-de",
"yahoo-en-GB",
"yahoo-es",
"yahoo-fi",
"yahoo-france",
"yahoo-fy-NL",
"yahoo-id",
"yahoo-in",
"yahoo-it",
"yahoo-jp",
"yahoo-jp-auctions",
"yahoo-mx",
"yahoo-sv-SE",
"yahoo-zh-TW",
"yandex",
"yandex-ru",
"yandex-slovari",
"yandex-tr",
"yandex.by",
"yandex.ru-be",
],
SOURCES: [
"abouthome",
"contextmenu",
@ -1383,6 +1237,40 @@ SearchCountMeasurement2.prototype = Object.freeze({
],
});
function SearchCountMeasurement2() {
SearchCountMeasurementBase.call(this);
}
SearchCountMeasurement2.prototype = Object.freeze({
__proto__: SearchCountMeasurementBase.prototype,
name: "counts",
version: 2,
});
function SearchCountMeasurement3() {
SearchCountMeasurementBase.call(this);
}
SearchCountMeasurement3.prototype = Object.freeze({
__proto__: SearchCountMeasurementBase.prototype,
name: "counts",
version: 3,
getEngines: function () {
return Services.search.getEngines();
},
getEngineID: function (engine) {
if (!engine) {
return "other";
}
if (engine.identifier) {
return engine.identifier;
}
return "other-" + engine.name;
},
});
this.SearchesProvider = function () {
Metrics.Provider.call(this);
};
@ -1394,6 +1282,7 @@ this.SearchesProvider.prototype = Object.freeze({
measurementTypes: [
SearchCountMeasurement1,
SearchCountMeasurement2,
SearchCountMeasurement3,
],
/**
@ -1412,8 +1301,7 @@ this.SearchesProvider.prototype = Object.freeze({
* Record that a search occurred.
*
* @param engine
* (string) The search engine used. If the search engine is unknown,
* the search will be attributed to "other".
* (nsISearchEngine) The search engine used.
* @param source
* (string) Where the search was initiated from. Must be one of the
* SearchCountMeasurement2.SOURCES values.
@ -1422,17 +1310,30 @@ this.SearchesProvider.prototype = Object.freeze({
* The promise is resolved when the storage operation completes.
*/
recordSearch: function (engine, source) {
let m = this.getMeasurement("counts", 2);
let m = this.getMeasurement("counts", 3);
if (m.SOURCES.indexOf(source) == -1) {
throw new Error("Unknown source for search: " + source);
}
let id = m.interestingEngines[engine] || "other";
let field = id + "." + source;
return this.enqueueStorageOperation(function recordSearch() {
return m.incrementDailyCounter(field);
});
let field = m.getEngineID(engine) + "." + source;
if (this.storage.hasFieldFromMeasurement(m.id, field,
this.storage.FIELD_DAILY_COUNTER)) {
let fieldID = this.storage.fieldIDFromMeasurement(m.id, field);
return this.enqueueStorageOperation(function recordSearchKnownField() {
return this.storage.incrementDailyCounterFromFieldID(fieldID);
}.bind(this));
}
// Otherwise, we first need to create the field.
return this.enqueueStorageOperation(function recordFieldAndSearch() {
// This function has to return a promise.
return Task.spawn(function () {
let fieldID = yield this.storage.registerField(m.id, field,
this.storage.FIELD_DAILY_COUNTER);
yield this.storage.incrementDailyCounterFromFieldID(fieldID);
}.bind(this));
}.bind(this));
},
});

View File

@ -17,13 +17,10 @@ const DEFAULT_ENGINES = [
];
function MockSearchCountMeasurement() {
bsp.SearchCountMeasurement2.call(this);
bsp.SearchCountMeasurement3.call(this);
}
MockSearchCountMeasurement.prototype = {
__proto__: bsp.SearchCountMeasurement2.prototype,
getDefaultEngines: function () {
return DEFAULT_ENGINES;
},
__proto__: bsp.SearchCountMeasurement3.prototype,
};
function MockSearchesProvider() {
@ -52,24 +49,29 @@ add_task(function test_record() {
let now = new Date();
for (let engine of DEFAULT_ENGINES) {
yield provider.recordSearch(engine.name, "abouthome");
yield provider.recordSearch(engine.name, "contextmenu");
yield provider.recordSearch(engine.name, "searchbar");
yield provider.recordSearch(engine.name, "urlbar");
// Record searches for all but one of our defaults, and one engine that's
// not a default.
for (let engine of DEFAULT_ENGINES.concat([{name: "Not Default", identifier: "notdef"}])) {
if (engine.identifier == "yahoo") {
continue;
}
yield provider.recordSearch(engine, "abouthome");
yield provider.recordSearch(engine, "contextmenu");
yield provider.recordSearch(engine, "searchbar");
yield provider.recordSearch(engine, "urlbar");
}
// Invalid sources should throw.
let errored = false;
try {
yield provider.recordSearch(DEFAULT_ENGINES[0].name, "bad source");
yield provider.recordSearch(DEFAULT_ENGINES[0], "bad source");
} catch (ex) {
errored = true;
} finally {
do_check_true(errored);
}
let m = provider.getMeasurement("counts", 2);
let m = provider.getMeasurement("counts", 3);
let data = yield m.getValues();
do_check_eq(data.days.size, 1);
do_check_true(data.days.hasDay(now));
@ -77,17 +79,27 @@ add_task(function test_record() {
let day = data.days.getDay(now);
for (let engine of DEFAULT_ENGINES) {
let identifier = engine.identifier;
if (identifier == "foobar") {
identifier = "other";
}
let expected = identifier != "yahoo";
for (let source of ["abouthome", "contextmenu", "searchbar", "urlbar"]) {
let field = identifier + "." + source;
do_check_true(day.has(field));
do_check_eq(day.get(field), 1);
if (expected) {
do_check_true(day.has(field));
do_check_eq(day.get(field), 1);
} else {
do_check_false(day.has(field));
}
}
}
// Also, check that our non-default engine contributed, with a computed
// identifier.
let identifier = "notdef";
for (let source of ["abouthome", "contextmenu", "searchbar", "urlbar"]) {
let field = identifier + "." + source;
do_check_true(day.has(field));
}
yield storage.close();
});
@ -96,7 +108,7 @@ add_task(function test_includes_other_fields() {
let provider = new MockSearchesProvider();
yield provider.init(storage);
let m = provider.getMeasurement("counts", 2);
let m = provider.getMeasurement("counts", 3);
// Register a search against a provider that isn't live in this session.
let id = yield m.storage.registerField(m.id, "test.searchbar",

View File

@ -583,10 +583,15 @@ BrowserTabActor.prototype = {
dbg_assert(this.actorID,
"tab should have an actorID.");
let windowUtils = this.window
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
let response = {
actor: this.actorID,
title: this.title,
url: this.url
url: this.url,
outerWindowID: windowUtils.outerWindowID
};
// Walk over tab actors added by extensions and add them to a new ActorPool.

View File

@ -30,7 +30,15 @@ function run_test() {
}
do_check_eq(gUpdateManager.activeUpdate.state, STATE_DOWNLOADING);
do_test_finished();
// Pause the download and reload the Update Manager with an empty update so
// the Application Update Service doesn't write the update xml files during
// xpcom-shutdown which will leave behind the test directory.
gAUS.pauseDownload();
writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), true);
writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
reloadUpdateManagerData();
do_timeout(TEST_CHECK_TIMEOUT, do_test_finished);
}
function end_test() {

View File

@ -440,6 +440,16 @@ MetroInput::InitTouchEventTouchList(WidgetTouchEvent* aEvent)
static_cast<void*>(&aEvent->touches));
}
bool
MetroInput::ShouldDeliverInputToRecognizer()
{
// If the event is destined for chrome deliver all events to the recognizer.
if (mChromeHitTestCacheForTouch) {
return true;
}
return mRecognizerWantsEvents;
}
// This event is raised when the user pushes the left mouse button, presses a
// pen to the surface, or presses a touch screen.
HRESULT
@ -483,44 +493,26 @@ MetroInput::OnPointerPressed(UI::Core::ICoreWindow* aSender,
// If this is the first touchstart of a touch session reset some
// tracking flags and dispatch the event with a custom callback
// so we can check preventDefault result.
mTouchStartDefaultPrevented = false;
mTouchMoveDefaultPrevented = false;
mContentConsumingTouch = false;
mRecognizerWantsEvents = true;
mIsFirstTouchMove = true;
mCancelable = true;
mTouchCancelSent = false;
InitTouchEventTouchList(touchEvent);
DispatchAsyncTouchEventWithCallback(touchEvent, &MetroInput::OnPointerPressedCallback);
} else {
InitTouchEventTouchList(touchEvent);
DispatchAsyncTouchEventIgnoreStatus(touchEvent);
mCanceledIds.Clear();
}
if (!mTouchStartDefaultPrevented) {
InitTouchEventTouchList(touchEvent);
DispatchAsyncTouchEvent(touchEvent);
if (ShouldDeliverInputToRecognizer()) {
mGestureRecognizer->ProcessDownEvent(currentPoint.Get());
}
return S_OK;
}
void
MetroInput::OnPointerPressedCallback()
{
nsEventStatus status = DeliverNextQueuedTouchEvent();
mTouchStartDefaultPrevented = (nsEventStatus_eConsumeNoDefault == status);
if (mTouchStartDefaultPrevented) {
// If content canceled the first touchstart don't generate any gesture based
// input - clear the recognizer state without sending any events.
mGestureRecognizer->CompleteGesture();
// Let the apz know content wants to consume touch events.
mWidget->ApzContentConsumingTouch();
}
}
void
MetroInput::AddPointerMoveDataToRecognizer(UI::Core::IPointerEventArgs* aArgs)
{
// Only feed move input to the recognizer if the first touchstart and
// subsequent touchmove return results were not eConsumeNoDefault.
if (!mTouchStartDefaultPrevented && !mTouchMoveDefaultPrevented) {
if (ShouldDeliverInputToRecognizer()) {
WRL::ComPtr<Foundation::Collections::IVector<UI::Input::PointerPoint*>>
pointerPoints;
aArgs->GetIntermediatePoints(pointerPoints.GetAddressOf());
@ -586,7 +578,7 @@ MetroInput::OnPointerMoved(UI::Core::ICoreWindow* aSender,
WidgetTouchEvent* touchEvent =
new WidgetTouchEvent(true, NS_TOUCH_MOVE, mWidget.Get());
InitTouchEventTouchList(touchEvent);
DispatchAsyncTouchEventIgnoreStatus(touchEvent);
DispatchAsyncTouchEvent(touchEvent);
}
touch = CreateDOMTouch(currentPoint.Get());
@ -597,13 +589,10 @@ MetroInput::OnPointerMoved(UI::Core::ICoreWindow* aSender,
WidgetTouchEvent* touchEvent =
new WidgetTouchEvent(true, NS_TOUCH_MOVE, mWidget.Get());
// If this is the first touch move of our session, we should check the result.
// Note we may lose some touch move data here for the recognizer since we want
// to wait until we have the result of the first touchmove dispatch. For gesture
// based events this shouldn't break anything.
// If this is the first touch move of our session, dispatch it now.
if (mIsFirstTouchMove) {
InitTouchEventTouchList(touchEvent);
DispatchAsyncTouchEventWithCallback(touchEvent, &MetroInput::OnFirstPointerMoveCallback);
DispatchAsyncTouchEvent(touchEvent);
mIsFirstTouchMove = false;
}
@ -612,22 +601,6 @@ MetroInput::OnPointerMoved(UI::Core::ICoreWindow* aSender,
return S_OK;
}
void
MetroInput::OnFirstPointerMoveCallback()
{
nsEventStatus status = DeliverNextQueuedTouchEvent();
mCancelable = false;
mTouchMoveDefaultPrevented = (nsEventStatus_eConsumeNoDefault == status);
// Let the apz know whether content wants to consume touch events
if (mTouchMoveDefaultPrevented) {
mWidget->ApzContentConsumingTouch();
// reset the recognizer
mGestureRecognizer->CompleteGesture();
} else if (!mTouchMoveDefaultPrevented && !mTouchStartDefaultPrevented) {
mWidget->ApzContentIgnoringTouch();
}
}
// This event is raised when the user lifts the left mouse button, lifts a
// pen from the surface, or lifts her/his finger from a touch screen.
HRESULT
@ -667,7 +640,7 @@ MetroInput::OnPointerReleased(UI::Core::ICoreWindow* aSender,
WidgetTouchEvent* touchEvent =
new WidgetTouchEvent(true, NS_TOUCH_MOVE, mWidget.Get());
InitTouchEventTouchList(touchEvent);
DispatchAsyncTouchEventIgnoreStatus(touchEvent);
DispatchAsyncTouchEvent(touchEvent);
}
// Remove this touch point from our map. Eventually all touch points are
@ -679,11 +652,9 @@ MetroInput::OnPointerReleased(UI::Core::ICoreWindow* aSender,
WidgetTouchEvent* touchEvent =
new WidgetTouchEvent(true, NS_TOUCH_END, mWidget.Get());
touchEvent->touches.AppendElement(CreateDOMTouch(currentPoint.Get()));
DispatchAsyncTouchEventIgnoreStatus(touchEvent);
DispatchAsyncTouchEvent(touchEvent);
// If content didn't cancel the first touchstart feed touchend data to the
// recognizer.
if (!mTouchStartDefaultPrevented && !mTouchMoveDefaultPrevented) {
if (ShouldDeliverInputToRecognizer()) {
mGestureRecognizer->ProcessUpEvent(currentPoint.Get());
}
@ -1179,7 +1150,7 @@ MetroInput::DeliverNextQueuedEventIgnoreStatus()
}
void
MetroInput::DispatchAsyncTouchEventIgnoreStatus(WidgetTouchEvent* aEvent)
MetroInput::DispatchAsyncTouchEvent(WidgetTouchEvent* aEvent)
{
aEvent->time = ::GetMessageTime();
mModifierKeyState.Update();
@ -1190,7 +1161,7 @@ MetroInput::DispatchAsyncTouchEventIgnoreStatus(WidgetTouchEvent* aEvent)
NS_DispatchToCurrentThread(runnable);
}
nsEventStatus
void
MetroInput::DeliverNextQueuedTouchEvent()
{
nsEventStatus status;
@ -1203,88 +1174,118 @@ MetroInput::DeliverNextQueuedTouchEvent()
/*
* We go through states here and make different decisions in each:
*
* 1) delivering first touchpoint touchstart or its first touchmove
* Our callers (OnFirstPointerMoveCallback, OnPointerPressedCallback) will
* check our result and set mTouchStartDefaultPrevented or
* mTouchMoveDefaultPrevented appropriately. Deliver touch events to the apz
* (ignoring return result) and to content and return the content event
* status result to our caller.
* 2) mTouchStartDefaultPrevented or mTouchMoveDefaultPrevented are true
* Deliver touch to content after transforming through the apz. Our callers
* handle calling cancel for the touch sequence on the apz.
* 3) mTouchStartDefaultPrevented and mTouchMoveDefaultPrevented are false
* Deliver events to the apz. If the apz returns eConsumeNoDefault dispatch
* a touchcancel to content and do not deliver any additional events there.
* (If the apz is doing something with the events we can save ourselves
* the overhead of delivering dom events.)
* 1) Hit test chrome on first touchstart
* If chrome is the target simplify event delivery from that point
* on by directing all input to chrome, bypassing the apz.
* 2) Process first touchpoint touchstart and touchmove
* Check the result and set mContentConsumingTouch appropriately. Deliver
* touch events to the apz (ignoring return result) and to content.
* 3) If mContentConsumingTouch is true: deliver touch to content after
* transforming through the apz. Also let the apz know content is
* consuming touch.
* 4) If mContentConsumingTouch is false: send a touchcancel to content
* and deliver all events to the apz. If the apz is doing something with
* the events we can save ourselves the overhead of delivering dom events.
*
* Notes:
* - never rely on the contents of mTouches here, since this is a delayed
* callback. mTouches will likely have been modified.
*/
// Test for chrome vs. content target. To do this we only use the first touch
// point since that will be the input batch target. Cache this for touch events
// since HitTestChrome has to send a dom event.
if (event->message == NS_TOUCH_START) {
if (mCancelable && event->message == NS_TOUCH_START) {
nsRefPtr<Touch> touch = event->touches[0];
LayoutDeviceIntPoint pt = LayoutDeviceIntPoint::FromUntyped(touch->mRefPoint);
bool apzIntersect = mWidget->HitTestAPZC(mozilla::ScreenPoint(pt.x, pt.y));
mChromeHitTestCacheForTouch = (apzIntersect && HitTestChrome(pt));
}
// Check if content called preventDefault on touchstart or first touchmove. If so
// and the event is destined for chrome, send the event. If destined for content,
// translate coordinates through the apz then send.
if (mTouchStartDefaultPrevented || mTouchMoveDefaultPrevented) {
if (!mChromeHitTestCacheForTouch) {
// ContentReceivedTouch has already been called so this shouldn't cause
// the apz to react. We still need to transform our coordinates though.
mWidget->ApzReceiveInputEvent(event);
}
// If this event is destined for chrome, deliver it directly there bypassing
// the apz.
if (!mCancelable && mChromeHitTestCacheForTouch) {
mWidget->DispatchEvent(event, status);
return status;
return;
}
// Forward event data to apz. If the apz consumes the event, don't forward to
// content if this is not a cancelable event.
WidgetTouchEvent transformedEvent(*event);
status = mWidget->ApzReceiveInputEvent(event, &transformedEvent);
if (!mCancelable && status == nsEventStatus_eConsumeNoDefault) {
if (!mTouchCancelSent) {
mTouchCancelSent = true;
DispatchTouchCancel();
// If we have yet to deliver the first touch start and touch move, deliver the
// event to both content and the apz. Ignore the apz's return result since we
// give content the option of saying it wants to consume touch for both events.
if (mCancelable) {
WidgetTouchEvent transformedEvent(*event);
mWidget->ApzReceiveInputEvent(event, &transformedEvent);
mWidget->DispatchEvent(mChromeHitTestCacheForTouch ? event : &transformedEvent, status);
if (event->message == NS_TOUCH_START) {
mContentConsumingTouch = (nsEventStatus_eConsumeNoDefault == status);
// Disable gesture based events (taps, swipes, rotation) if
// preventDefault is called on touchstart.
mRecognizerWantsEvents = !(nsEventStatus_eConsumeNoDefault == status);
} else if (event->message == NS_TOUCH_MOVE) {
mCancelable = false;
// Add this result to to our content comsuming flag
if (!mContentConsumingTouch) {
mContentConsumingTouch = (nsEventStatus_eConsumeNoDefault == status);
}
// Let the apz know if content wants to consume touch events, or cancel
// the touch block for content.
if (mContentConsumingTouch) {
mWidget->ApzContentConsumingTouch();
} else {
mWidget->ApzContentIgnoringTouch();
DispatchTouchCancel(&transformedEvent);
}
}
return status;
// If content is consuming touch don't generate any gesture based
// input - clear the recognizer state without sending any events.
if (!ShouldDeliverInputToRecognizer()) {
mGestureRecognizer->CompleteGesture();
}
return;
}
// Deliver event. If this is destined for chrome, use the untransformed event
// data, if it's destined for content, use the transformed event.
mWidget->DispatchEvent(!mChromeHitTestCacheForTouch ? &transformedEvent : event, status);
return status;
// If content called preventDefault on touchstart or first touchmove send
// the event to content.
if (mContentConsumingTouch) {
// ContentReceivedTouch has already been called in the mCancelable block
// above so this shouldn't cause the apz to react. We still need to
// transform our coordinates though.
mWidget->ApzReceiveInputEvent(event);
mWidget->DispatchEvent(event, status);
return;
}
// Forward event data to apz.
mWidget->ApzReceiveInputEvent(event);
}
void
MetroInput::DispatchTouchCancel()
MetroInput::DispatchTouchCancel(WidgetTouchEvent* aEvent)
{
LogFunction();
// From the spec: The touch point or points that were removed must be
// included in the changedTouches attribute of the TouchEvent, and must
// not be included in the touches and targetTouches attributes.
// (We are 'removing' all touch points that have been sent to content
// thus far.)
MOZ_ASSERT(aEvent);
// Send a touchcancel for each pointer id we have a corresponding start
// for. Note we can't rely on mTouches here since touchends remove points
// from it. The only time we end up in here is if the apz is consuming
// events, so this array shouldn't be very large.
WidgetTouchEvent touchEvent(true, NS_TOUCH_CANCEL, mWidget.Get());
InitTouchEventTouchList(&touchEvent);
mWidget->DispatchEvent(&touchEvent, sThrowawayStatus);
}
nsTArray< nsRefPtr<dom::Touch> >& touches = aEvent->touches;
for (uint32_t i = 0; i < touches.Length(); ++i) {
dom::Touch* touch = touches[i];
if (!touch) {
continue;
}
int32_t id = touch->Identifier();
if (mCanceledIds.Contains(id)) {
continue;
}
mCanceledIds.AppendElement(id);
touchEvent.touches.AppendElement(touch);
}
if (!touchEvent.touches.Length()) {
return;
}
void
MetroInput::DispatchAsyncTouchEventWithCallback(WidgetTouchEvent* aEvent,
void (MetroInput::*Callback)())
{
aEvent->time = ::GetMessageTime();
mModifierKeyState.Update();
mModifierKeyState.InitInputEvent(*aEvent);
mInputEventQueue.Push(aEvent);
nsCOMPtr<nsIRunnable> runnable =
NS_NewRunnableMethod(this, Callback);
NS_DispatchToCurrentThread(runnable);
mWidget->DispatchEvent(&touchEvent, sThrowawayStatus);
}
void

View File

@ -183,6 +183,7 @@ private:
uint32_t aMagEventType,
uint32_t aRotEventType);
uint16_t ProcessInputTypeForGesture(IEdgeGestureEventArgs* aArgs);
bool ShouldDeliverInputToRecognizer();
// The W3C spec states that "whether preventDefault has been called" should
// be tracked on a per-touchpoint basis, but it also states that touchstart
@ -207,11 +208,11 @@ private:
// events will be generated based on the touchstart and touchend events.
// For example, a set of mousemove, mousedown, and mouseup events might
// be sent if a tap is detected.
bool mTouchStartDefaultPrevented;
bool mTouchMoveDefaultPrevented;
bool mContentConsumingTouch;
bool mIsFirstTouchMove;
bool mCancelable;
bool mTouchCancelSent;
bool mRecognizerWantsEvents;
nsTArray<uint32_t> mCanceledIds;
// In the old Win32 way of doing things, we would receive a WM_TOUCH event
// that told us the state of every touchpoint on the touch surface. If
@ -273,21 +274,15 @@ private:
// Async event dispatching
void DispatchAsyncEventIgnoreStatus(WidgetInputEvent* aEvent);
void DispatchAsyncTouchEventIgnoreStatus(WidgetTouchEvent* aEvent);
void DispatchAsyncTouchEventWithCallback(WidgetTouchEvent* aEvent,
void (MetroInput::*Callback)());
void DispatchAsyncTouchEvent(WidgetTouchEvent* aEvent);
// Async event callbacks
void DeliverNextQueuedEventIgnoreStatus();
nsEventStatus DeliverNextQueuedTouchEvent();
// Misc. specialty async callbacks
void OnPointerPressedCallback();
void OnFirstPointerMoveCallback();
void DeliverNextQueuedTouchEvent();
// Sync event dispatching
void DispatchEventIgnoreStatus(WidgetGUIEvent* aEvent);
void DispatchTouchCancel();
void DispatchTouchCancel(WidgetTouchEvent* aEvent);
nsDeque mInputEventQueue;
static nsEventStatus sThrowawayStatus;