From 7fe64cdda2e02c1114766abfa25d8126a0c74192 Mon Sep 17 00:00:00 2001 From: Chenxia Liu Date: Thu, 19 Dec 2013 16:30:47 -0800 Subject: [PATCH 1/8] Bug 910189: Part 1 - Add "restore defaults" hooks into settings. r=margaret --HG-- rename : mobile/android/base/resources/xml/preferences_search.xml => mobile/android/base/resources/xml-v11/preferences_search.xml --- .../base/locales/en-US/android_strings.dtd | 3 + .../preferences/GeckoPreferenceFragment.java | 83 +++++++++++++++---- .../menu-v11/preferences_search_menu.xml | 11 +++ .../resources/xml-v11/preferences_search.xml | 27 ++++++ .../base/resources/xml/preferences_search.xml | 7 ++ mobile/android/base/strings.xml.in | 4 + 6 files changed, 121 insertions(+), 14 deletions(-) create mode 100644 mobile/android/base/resources/menu-v11/preferences_search_menu.xml create mode 100644 mobile/android/base/resources/xml-v11/preferences_search.xml diff --git a/mobile/android/base/locales/en-US/android_strings.dtd b/mobile/android/base/locales/en-US/android_strings.dtd index 9ada4a0cf4e..2772224df0f 100644 --- a/mobile/android/base/locales/en-US/android_strings.dtd +++ b/mobile/android/base/locales/en-US/android_strings.dtd @@ -73,6 +73,9 @@ + + + diff --git a/mobile/android/base/preferences/GeckoPreferenceFragment.java b/mobile/android/base/preferences/GeckoPreferenceFragment.java index db2a062114e..0f5fdbb48fc 100644 --- a/mobile/android/base/preferences/GeckoPreferenceFragment.java +++ b/mobile/android/base/preferences/GeckoPreferenceFragment.java @@ -5,9 +5,12 @@ package org.mozilla.gecko.preferences; +import java.lang.reflect.Field; + import org.mozilla.gecko.R; import org.mozilla.gecko.PrefsHelper; +import android.app.Activity; import android.preference.Preference; import android.preference.PreferenceActivity; import android.preference.PreferenceCategory; @@ -15,6 +18,9 @@ import android.preference.PreferenceFragment; import android.preference.PreferenceScreen; import android.os.Bundle; import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.ViewConfiguration; /* A simple implementation of PreferenceFragment for large screen devices * This will strip category headers (so that they aren't shown to the user twice) @@ -28,23 +34,14 @@ public class GeckoPreferenceFragment extends PreferenceFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - String resourceName = getArguments().getString("resource"); - int res = 0; - if (resourceName != null) { - // Fetch resource id by resource name. - res = getActivity().getResources().getIdentifier(resourceName, - "xml", - getActivity().getPackageName()); + int res = getResource(); + + // Display a menu for Search preferences. + if (res == R.xml.preferences_search) { + setHasOptionsMenu(true); } - if (res == 0) { - // The resource was invalid. Use the default resource. - Log.e(LOGTAG, "Failed to find resource: " + resourceName + ". Displaying default settings."); - - boolean isMultiPane = ((PreferenceActivity) getActivity()).onIsMultiPane(); - res = isMultiPane ? R.xml.preferences_customize_tablet : R.xml.preferences; - } addPreferencesFromResource(res); PreferenceScreen screen = getPreferenceScreen(); @@ -52,6 +49,39 @@ public class GeckoPreferenceFragment extends PreferenceFragment { mPrefsRequestId = ((GeckoPreferences)getActivity()).setupPreferences(screen); } + /* + * Get the resource from Fragment arguments and return it. + * + * If no resource can be found, return the resource id of the default preference screen. + */ + private int getResource() { + int resid = 0; + + String resourceName = getArguments().getString("resource"); + if (resourceName != null) { + // Fetch resource id by resource name. + resid = getActivity().getResources().getIdentifier(resourceName, + "xml", + getActivity().getPackageName()); + } + + if (resid == 0) { + // The resource was invalid. Use the default resource. + Log.e(LOGTAG, "Failed to find resource: " + resourceName + ". Displaying default settings."); + + boolean isMultiPane = ((PreferenceActivity) getActivity()).onIsMultiPane(); + resid = isMultiPane ? R.xml.preferences_customize_tablet : R.xml.preferences; + } + + return resid; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.preferences_search_menu, menu); + } + @Override public void onDestroy() { super.onDestroy(); @@ -59,4 +89,29 @@ public class GeckoPreferenceFragment extends PreferenceFragment { PrefsHelper.removeObserver(mPrefsRequestId); } } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + showOverflowMenu(activity); + } + + /* + * Force the overflow 3-dot menu to be displayed if it isn't already displayed. + * + * This is an ugly hack for 4.0+ Android devices that don't have a dedicated menu button + * because Android does not provide a public API to display the ActionBar overflow menu. + */ + private void showOverflowMenu(Activity activity) { + try { + ViewConfiguration config = ViewConfiguration.get(activity); + Field menuOverflow = ViewConfiguration.class.getDeclaredField("sHasPermanentMenuKey"); + if (menuOverflow != null) { + menuOverflow.setAccessible(true); + menuOverflow.setBoolean(config, false); + } + } catch (Exception e) { + Log.d(LOGTAG, "Failed to force overflow menu, ignoring."); + } + } } diff --git a/mobile/android/base/resources/menu-v11/preferences_search_menu.xml b/mobile/android/base/resources/menu-v11/preferences_search_menu.xml new file mode 100644 index 00000000000..9a3cdc72a88 --- /dev/null +++ b/mobile/android/base/resources/menu-v11/preferences_search_menu.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/mobile/android/base/resources/xml-v11/preferences_search.xml b/mobile/android/base/resources/xml-v11/preferences_search.xml new file mode 100644 index 00000000000..cda787acf0b --- /dev/null +++ b/mobile/android/base/resources/xml-v11/preferences_search.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + diff --git a/mobile/android/base/resources/xml/preferences_search.xml b/mobile/android/base/resources/xml/preferences_search.xml index cda787acf0b..0f594d3de5f 100644 --- a/mobile/android/base/resources/xml/preferences_search.xml +++ b/mobile/android/base/resources/xml/preferences_search.xml @@ -16,6 +16,13 @@ + + + + + + &pref_category_datareporting; &pref_category_installed_search_engines; &pref_category_add_search_providers; + &pref_category_search_restore_defaults; + &pref_search_restore_defaults; + &pref_search_restore_defaults_summary; &pref_search_tip; + &pref_category_devtools; &pref_developer_remotedebugging; &pref_developer_remotedebugging_docs; From f5ae409e5809bddd73ded9571b9ff9b7f3a415bf Mon Sep 17 00:00:00 2001 From: Chenxia Liu Date: Thu, 19 Dec 2013 16:30:47 -0800 Subject: [PATCH 2/8] Bug 910189: Part 2 - Enable removing of default engines. r=margaret --- .../preferences/SearchEnginePreference.java | 12 -------- .../preferences/SearchPreferenceCategory.java | 2 +- .../android/base/tests/testDistribution.java | 2 +- mobile/android/chrome/content/browser.js | 28 ++----------------- 4 files changed, 5 insertions(+), 39 deletions(-) diff --git a/mobile/android/base/preferences/SearchEnginePreference.java b/mobile/android/base/preferences/SearchEnginePreference.java index 78f7be5ebff..37f39d3e9c9 100644 --- a/mobile/android/base/preferences/SearchEnginePreference.java +++ b/mobile/android/base/preferences/SearchEnginePreference.java @@ -40,8 +40,6 @@ public class SearchEnginePreference extends Preference implements View.OnLongCli // Specifies if this engine is configured as the default search engine. private boolean mIsDefaultEngine; - // Specifies if this engine is one of the ones bundled with the app, which cannot be deleted. - private boolean mIsImmutableEngine; // Dialog element labels. private String[] mDialogItems; @@ -121,12 +119,7 @@ public class SearchEnginePreference extends Preference implements View.OnLongCli public void setSearchEngineFromJSON(JSONObject geckoEngineJSON) throws JSONException { final String engineName = geckoEngineJSON.getString("name"); final SpannableString titleSpannable = new SpannableString(engineName); - mIsImmutableEngine = geckoEngineJSON.getBoolean("immutable"); - if (mIsImmutableEngine) { - // Delete the "Remove" option from the menu. - mDialogItems = new String[] { getContext().getResources().getString(R.string.pref_search_set_default) }; - } setTitle(titleSpannable); final String iconURI = geckoEngineJSON.getString("iconURI"); @@ -176,11 +169,6 @@ public class SearchEnginePreference extends Preference implements View.OnLongCli return; } - // If we are both default and immutable, we have no enabled items to show on the menu - abort. - if (mIsDefaultEngine && mIsImmutableEngine) { - return; - } - final AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); builder.setTitle(getTitle().toString()); builder.setItems(mDialogItems, new DialogInterface.OnClickListener() { diff --git a/mobile/android/base/preferences/SearchPreferenceCategory.java b/mobile/android/base/preferences/SearchPreferenceCategory.java index 9374c968387..0ddc1268178 100644 --- a/mobile/android/base/preferences/SearchPreferenceCategory.java +++ b/mobile/android/base/preferences/SearchPreferenceCategory.java @@ -45,7 +45,7 @@ public class SearchPreferenceCategory extends PreferenceCategory implements Geck // Request list of search engines from Gecko. GeckoAppShell.registerEventListener("SearchEngines:Data", this); - GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:Get", null)); + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:GetVisible", null)); } @Override diff --git a/mobile/android/base/tests/testDistribution.java b/mobile/android/base/tests/testDistribution.java index 0c2787ef1f8..4b739a08aa7 100644 --- a/mobile/android/base/tests/testDistribution.java +++ b/mobile/android/base/tests/testDistribution.java @@ -159,7 +159,7 @@ public class testDistribution extends ContentProviderTest { private void checkSearchPlugin() { Actions.RepeatedEventExpecter eventExpecter = mActions.expectGeckoEvent("SearchEngines:Data"); - mActions.sendGeckoEvent("SearchEngines:Get", null); + mActions.sendGeckoEvent("SearchEngines:GetVisible", null); try { JSONObject data = new JSONObject(eventExpecter.blockForEventData()); diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index 6e6b640b19a..e7cf3dd6208 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -6608,7 +6608,6 @@ var SearchEngines = { 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); @@ -6651,7 +6650,6 @@ var SearchEngines = { 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"); @@ -6660,28 +6658,19 @@ var SearchEngines = { }, // Fetch list of search engines. all ? All engines : Visible engines only. - _handleSearchEnginesGet: function _handleSearchEnginesGet(rv, all) { + _handleSearchEnginesGetVisible: function _handleSearchEnginesGetVisible(rv, all) { if (!Components.isSuccessCode(rv)) { Cu.reportError("Could not initialize search service, bailing out."); return; } - let engineData; - if (all) { - engineData = Services.search.getEngines({}); - } else { - engineData = Services.search.getVisibleEngines({}); - } - - // These engines are the bundled ones - they may not be uninstalled. - let immutableEngines = Services.search.getDefaultEngines(); + let engineData = Services.search.getVisibleEngines({}); let searchEngines = engineData.map(function (engine) { return { name: engine.name, identifier: engine.identifier, iconURI: (engine.iconURI ? engine.iconURI.spec : null), - hidden: engine.hidden, - immutable: immutableEngines.indexOf(engine) != -1 + hidden: engine.hidden }; }); @@ -6718,13 +6707,6 @@ var SearchEngines = { } catch (e) {} }, - _handleSearchEnginesGetAll: function _handleSearchEnginesGetAll(rv) { - this._handleSearchEnginesGet(rv, true); - }, - _handleSearchEnginesGetVisible: function _handleSearchEnginesGetVisible(rv) { - this._handleSearchEnginesGet(rv, false) - }, - // Helper method to extract the engine name from a JSON. Simplifies the observe function. _extractEngineFromJSON: function _extractEngineFromJSON(aData) { let data = JSON.parse(aData); @@ -6740,10 +6722,6 @@ var SearchEngines = { 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": engine = this._extractEngineFromJSON(aData); // Move the new default search engine to the top of the search engine list. From 3eab5643d55aa033ff64326be666f14a8b09994a Mon Sep 17 00:00:00 2001 From: Chenxia Liu Date: Thu, 19 Dec 2013 16:30:47 -0800 Subject: [PATCH 3/8] Bug 910189: Part 3 - Restore default search engines (no dialog). r=margaret --- .../preferences/GeckoPreferenceFragment.java | 1 + .../base/preferences/GeckoPreferences.java | 31 +++++++++++++++++-- .../preferences/SearchPreferenceCategory.java | 14 ++++++--- mobile/android/chrome/content/browser.js | 23 +++++++++----- 4 files changed, 54 insertions(+), 15 deletions(-) diff --git a/mobile/android/base/preferences/GeckoPreferenceFragment.java b/mobile/android/base/preferences/GeckoPreferenceFragment.java index 0f5fdbb48fc..a564c9f98d0 100644 --- a/mobile/android/base/preferences/GeckoPreferenceFragment.java +++ b/mobile/android/base/preferences/GeckoPreferenceFragment.java @@ -20,6 +20,7 @@ import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; +import android.view.MenuItem; import android.view.ViewConfiguration; /* A simple implementation of PreferenceFragment for large screen devices diff --git a/mobile/android/base/preferences/GeckoPreferences.java b/mobile/android/base/preferences/GeckoPreferences.java index fe434d6feb5..b665159f1cb 100644 --- a/mobile/android/base/preferences/GeckoPreferences.java +++ b/mobile/android/base/preferences/GeckoPreferences.java @@ -77,7 +77,9 @@ public class GeckoPreferences private boolean mInitialized = false; private int mPrefsRequestId = 0; - // These match keys in resources/xml/preferences.xml.in. + // These match keys in resources/xml*/preferences*.xml + private static String PREFS_SEARCH_RESTORE_DEFAULTS = NON_PREF_PREFIX + "search.restore_defaults"; + private static String PREFS_ANNOUNCEMENTS_ENABLED = NON_PREF_PREFIX + "privacy.announcements.enabled"; private static String PREFS_DATA_REPORTING_PREFERENCES = NON_PREF_PREFIX + "datareporting.preferences"; private static String PREFS_TELEMETRY_ENABLED = "datareporting.telemetry.enabled"; @@ -389,6 +391,14 @@ public class GeckoPreferences preferences.removePreference(pref); i--; continue; + } else if (PREFS_SEARCH_RESTORE_DEFAULTS.equals(key)) { + pref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + GeckoPreferences.this.restoreDefaultSearchEngines(); + return true; + } + }); } // Some Preference UI elements are not actually preferences, @@ -404,14 +414,31 @@ public class GeckoPreferences } } + /** + * Restore default search engines in Gecko and retrigger a search engine refresh. + */ + protected void restoreDefaultSearchEngines() { + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:RestoreDefaults", null)); + + // Send message to Gecko to get engines. SearchPreferenceCategory listens for the response. + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:GetVisible", null)); + } + @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { + int itemId = item.getItemId(); + switch (itemId) { case android.R.id.home: finish(); return true; } + // Generated R.id.* apparently aren't constant expressions, so they can't be switched. + if (itemId == R.id.restore_defaults) { + restoreDefaultSearchEngines(); + return true; + } + return super.onOptionsItemSelected(item); } diff --git a/mobile/android/base/preferences/SearchPreferenceCategory.java b/mobile/android/base/preferences/SearchPreferenceCategory.java index 0ddc1268178..7ab63058447 100644 --- a/mobile/android/base/preferences/SearchPreferenceCategory.java +++ b/mobile/android/base/preferences/SearchPreferenceCategory.java @@ -43,18 +43,19 @@ public class SearchPreferenceCategory extends PreferenceCategory implements Geck // Ensures default engine remains at top of list. setOrderingAsAdded(true); - // Request list of search engines from Gecko. + // Register for SearchEngines messages and request list of search engines from Gecko. GeckoAppShell.registerEventListener("SearchEngines:Data", this); GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:GetVisible", null)); } + @Override + protected void onPrepareForRemoval() { + GeckoAppShell.unregisterEventListener("SearchEngines:Data", this); + } + @Override public void handleMessage(String event, final JSONObject data) { if (event.equals("SearchEngines:Data")) { - // We are no longer interested in this event from Gecko, as we do not request it again with - // this instance. - GeckoAppShell.unregisterEventListener("SearchEngines:Data", this); - // Parse engines array from JSON. JSONArray engines; try { @@ -64,6 +65,9 @@ public class SearchPreferenceCategory extends PreferenceCategory implements Geck return; } + // Clear the preferences category from this thread. + this.removeAll(); + // Create an element in this PreferenceCategory for each engine. for (int i = 0; i < engines.length(); i++) { try { diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index e7cf3dd6208..6fccdff3a6e 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -6609,8 +6609,9 @@ var SearchEngines = { init: function init() { Services.obs.addObserver(this, "SearchEngines:Add", false); Services.obs.addObserver(this, "SearchEngines:GetVisible", false); - Services.obs.addObserver(this, "SearchEngines:SetDefault", false); Services.obs.addObserver(this, "SearchEngines:Remove", false); + Services.obs.addObserver(this, "SearchEngines:RestoreDefaults", false); + Services.obs.addObserver(this, "SearchEngines:SetDefault", false); let filter = { matches: function (aElement) { @@ -6651,8 +6652,9 @@ var SearchEngines = { uninit: function uninit() { Services.obs.removeObserver(this, "SearchEngines:Add"); Services.obs.removeObserver(this, "SearchEngines:GetVisible"); - Services.obs.removeObserver(this, "SearchEngines:SetDefault"); Services.obs.removeObserver(this, "SearchEngines:Remove"); + Services.obs.removeObserver(this, "SearchEngines:RestoreDefaults"); + Services.obs.removeObserver(this, "SearchEngines:SetDefault"); if (this._contextMenuId != null) NativeWindow.contextmenus.remove(this._contextMenuId); }, @@ -6722,12 +6724,6 @@ var SearchEngines = { case "SearchEngines:GetVisible": Services.search.init(this._handleSearchEnginesGetVisible.bind(this)); break; - case "SearchEngines:SetDefault": - engine = this._extractEngineFromJSON(aData); - // Move the new default search engine to the top of the search engine list. - Services.search.moveEngine(engine, 0); - Services.search.defaultEngine = engine; - break; case "SearchEngines:Remove": // Make sure the engine isn't hidden before removing it, to make sure it's // visible if the user later re-adds it (works around bug 341833) @@ -6735,6 +6731,17 @@ var SearchEngines = { engine.hidden = false; Services.search.removeEngine(engine); break; + case "SearchEngines:RestoreDefaults": + // Un-hides all default engines. + Services.search.restoreDefaultEngines(); + break; + case "SearchEngines:SetDefault": + engine = this._extractEngineFromJSON(aData); + // Move the new default search engine to the top of the search engine list. + Services.search.moveEngine(engine, 0); + Services.search.defaultEngine = engine; + break; + default: dump("Unexpected message type observed: " + aTopic); break; From 49536a324a131a22a7a4be9b32c319f77d27ea9b Mon Sep 17 00:00:00 2001 From: Wes Kocher Date: Thu, 19 Dec 2013 17:10:26 -0800 Subject: [PATCH 4/8] Backed out 3 changesets (bug 910189) for breaking android builds Backed out changeset 119e40b61023 (bug 910189) Backed out changeset 1c778892acc6 (bug 910189) Backed out changeset 96816b5d7299 (bug 910189) --- .../base/locales/en-US/android_strings.dtd | 3 - .../preferences/GeckoPreferenceFragment.java | 84 ++++--------------- .../base/preferences/GeckoPreferences.java | 31 +------ .../preferences/SearchEnginePreference.java | 12 +++ .../preferences/SearchPreferenceCategory.java | 16 ++-- .../menu-v11/preferences_search_menu.xml | 11 --- .../resources/xml-v11/preferences_search.xml | 27 ------ .../base/resources/xml/preferences_search.xml | 7 -- mobile/android/base/strings.xml.in | 4 - .../android/base/tests/testDistribution.java | 2 +- mobile/android/chrome/content/browser.js | 51 +++++++---- 11 files changed, 68 insertions(+), 180 deletions(-) delete mode 100644 mobile/android/base/resources/menu-v11/preferences_search_menu.xml delete mode 100644 mobile/android/base/resources/xml-v11/preferences_search.xml diff --git a/mobile/android/base/locales/en-US/android_strings.dtd b/mobile/android/base/locales/en-US/android_strings.dtd index 2772224df0f..9ada4a0cf4e 100644 --- a/mobile/android/base/locales/en-US/android_strings.dtd +++ b/mobile/android/base/locales/en-US/android_strings.dtd @@ -73,9 +73,6 @@ - - - diff --git a/mobile/android/base/preferences/GeckoPreferenceFragment.java b/mobile/android/base/preferences/GeckoPreferenceFragment.java index a564c9f98d0..db2a062114e 100644 --- a/mobile/android/base/preferences/GeckoPreferenceFragment.java +++ b/mobile/android/base/preferences/GeckoPreferenceFragment.java @@ -5,12 +5,9 @@ package org.mozilla.gecko.preferences; -import java.lang.reflect.Field; - import org.mozilla.gecko.R; import org.mozilla.gecko.PrefsHelper; -import android.app.Activity; import android.preference.Preference; import android.preference.PreferenceActivity; import android.preference.PreferenceCategory; @@ -18,10 +15,6 @@ import android.preference.PreferenceFragment; import android.preference.PreferenceScreen; import android.os.Bundle; import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.ViewConfiguration; /* A simple implementation of PreferenceFragment for large screen devices * This will strip category headers (so that they aren't shown to the user twice) @@ -35,14 +28,23 @@ public class GeckoPreferenceFragment extends PreferenceFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + String resourceName = getArguments().getString("resource"); - int res = getResource(); - - // Display a menu for Search preferences. - if (res == R.xml.preferences_search) { - setHasOptionsMenu(true); + int res = 0; + if (resourceName != null) { + // Fetch resource id by resource name. + res = getActivity().getResources().getIdentifier(resourceName, + "xml", + getActivity().getPackageName()); } + if (res == 0) { + // The resource was invalid. Use the default resource. + Log.e(LOGTAG, "Failed to find resource: " + resourceName + ". Displaying default settings."); + + boolean isMultiPane = ((PreferenceActivity) getActivity()).onIsMultiPane(); + res = isMultiPane ? R.xml.preferences_customize_tablet : R.xml.preferences; + } addPreferencesFromResource(res); PreferenceScreen screen = getPreferenceScreen(); @@ -50,39 +52,6 @@ public class GeckoPreferenceFragment extends PreferenceFragment { mPrefsRequestId = ((GeckoPreferences)getActivity()).setupPreferences(screen); } - /* - * Get the resource from Fragment arguments and return it. - * - * If no resource can be found, return the resource id of the default preference screen. - */ - private int getResource() { - int resid = 0; - - String resourceName = getArguments().getString("resource"); - if (resourceName != null) { - // Fetch resource id by resource name. - resid = getActivity().getResources().getIdentifier(resourceName, - "xml", - getActivity().getPackageName()); - } - - if (resid == 0) { - // The resource was invalid. Use the default resource. - Log.e(LOGTAG, "Failed to find resource: " + resourceName + ". Displaying default settings."); - - boolean isMultiPane = ((PreferenceActivity) getActivity()).onIsMultiPane(); - resid = isMultiPane ? R.xml.preferences_customize_tablet : R.xml.preferences; - } - - return resid; - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - inflater.inflate(R.menu.preferences_search_menu, menu); - } - @Override public void onDestroy() { super.onDestroy(); @@ -90,29 +59,4 @@ public class GeckoPreferenceFragment extends PreferenceFragment { PrefsHelper.removeObserver(mPrefsRequestId); } } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - showOverflowMenu(activity); - } - - /* - * Force the overflow 3-dot menu to be displayed if it isn't already displayed. - * - * This is an ugly hack for 4.0+ Android devices that don't have a dedicated menu button - * because Android does not provide a public API to display the ActionBar overflow menu. - */ - private void showOverflowMenu(Activity activity) { - try { - ViewConfiguration config = ViewConfiguration.get(activity); - Field menuOverflow = ViewConfiguration.class.getDeclaredField("sHasPermanentMenuKey"); - if (menuOverflow != null) { - menuOverflow.setAccessible(true); - menuOverflow.setBoolean(config, false); - } - } catch (Exception e) { - Log.d(LOGTAG, "Failed to force overflow menu, ignoring."); - } - } } diff --git a/mobile/android/base/preferences/GeckoPreferences.java b/mobile/android/base/preferences/GeckoPreferences.java index b665159f1cb..fe434d6feb5 100644 --- a/mobile/android/base/preferences/GeckoPreferences.java +++ b/mobile/android/base/preferences/GeckoPreferences.java @@ -77,9 +77,7 @@ public class GeckoPreferences private boolean mInitialized = false; private int mPrefsRequestId = 0; - // These match keys in resources/xml*/preferences*.xml - private static String PREFS_SEARCH_RESTORE_DEFAULTS = NON_PREF_PREFIX + "search.restore_defaults"; - + // These match keys in resources/xml/preferences.xml.in. private static String PREFS_ANNOUNCEMENTS_ENABLED = NON_PREF_PREFIX + "privacy.announcements.enabled"; private static String PREFS_DATA_REPORTING_PREFERENCES = NON_PREF_PREFIX + "datareporting.preferences"; private static String PREFS_TELEMETRY_ENABLED = "datareporting.telemetry.enabled"; @@ -391,14 +389,6 @@ public class GeckoPreferences preferences.removePreference(pref); i--; continue; - } else if (PREFS_SEARCH_RESTORE_DEFAULTS.equals(key)) { - pref.setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - GeckoPreferences.this.restoreDefaultSearchEngines(); - return true; - } - }); } // Some Preference UI elements are not actually preferences, @@ -414,31 +404,14 @@ public class GeckoPreferences } } - /** - * Restore default search engines in Gecko and retrigger a search engine refresh. - */ - protected void restoreDefaultSearchEngines() { - GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:RestoreDefaults", null)); - - // Send message to Gecko to get engines. SearchPreferenceCategory listens for the response. - GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:GetVisible", null)); - } - @Override public boolean onOptionsItemSelected(MenuItem item) { - int itemId = item.getItemId(); - switch (itemId) { + switch (item.getItemId()) { case android.R.id.home: finish(); return true; } - // Generated R.id.* apparently aren't constant expressions, so they can't be switched. - if (itemId == R.id.restore_defaults) { - restoreDefaultSearchEngines(); - return true; - } - return super.onOptionsItemSelected(item); } diff --git a/mobile/android/base/preferences/SearchEnginePreference.java b/mobile/android/base/preferences/SearchEnginePreference.java index 37f39d3e9c9..78f7be5ebff 100644 --- a/mobile/android/base/preferences/SearchEnginePreference.java +++ b/mobile/android/base/preferences/SearchEnginePreference.java @@ -40,6 +40,8 @@ public class SearchEnginePreference extends Preference implements View.OnLongCli // Specifies if this engine is configured as the default search engine. private boolean mIsDefaultEngine; + // Specifies if this engine is one of the ones bundled with the app, which cannot be deleted. + private boolean mIsImmutableEngine; // Dialog element labels. private String[] mDialogItems; @@ -119,7 +121,12 @@ public class SearchEnginePreference extends Preference implements View.OnLongCli public void setSearchEngineFromJSON(JSONObject geckoEngineJSON) throws JSONException { final String engineName = geckoEngineJSON.getString("name"); final SpannableString titleSpannable = new SpannableString(engineName); + mIsImmutableEngine = geckoEngineJSON.getBoolean("immutable"); + if (mIsImmutableEngine) { + // Delete the "Remove" option from the menu. + mDialogItems = new String[] { getContext().getResources().getString(R.string.pref_search_set_default) }; + } setTitle(titleSpannable); final String iconURI = geckoEngineJSON.getString("iconURI"); @@ -169,6 +176,11 @@ public class SearchEnginePreference extends Preference implements View.OnLongCli return; } + // If we are both default and immutable, we have no enabled items to show on the menu - abort. + if (mIsDefaultEngine && mIsImmutableEngine) { + return; + } + final AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); builder.setTitle(getTitle().toString()); builder.setItems(mDialogItems, new DialogInterface.OnClickListener() { diff --git a/mobile/android/base/preferences/SearchPreferenceCategory.java b/mobile/android/base/preferences/SearchPreferenceCategory.java index 7ab63058447..9374c968387 100644 --- a/mobile/android/base/preferences/SearchPreferenceCategory.java +++ b/mobile/android/base/preferences/SearchPreferenceCategory.java @@ -43,19 +43,18 @@ public class SearchPreferenceCategory extends PreferenceCategory implements Geck // Ensures default engine remains at top of list. setOrderingAsAdded(true); - // Register for SearchEngines messages and request list of search engines from Gecko. + // Request list of search engines from Gecko. GeckoAppShell.registerEventListener("SearchEngines:Data", this); - GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:GetVisible", null)); - } - - @Override - protected void onPrepareForRemoval() { - GeckoAppShell.unregisterEventListener("SearchEngines:Data", this); + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:Get", null)); } @Override public void handleMessage(String event, final JSONObject data) { if (event.equals("SearchEngines:Data")) { + // We are no longer interested in this event from Gecko, as we do not request it again with + // this instance. + GeckoAppShell.unregisterEventListener("SearchEngines:Data", this); + // Parse engines array from JSON. JSONArray engines; try { @@ -65,9 +64,6 @@ public class SearchPreferenceCategory extends PreferenceCategory implements Geck return; } - // Clear the preferences category from this thread. - this.removeAll(); - // Create an element in this PreferenceCategory for each engine. for (int i = 0; i < engines.length(); i++) { try { diff --git a/mobile/android/base/resources/menu-v11/preferences_search_menu.xml b/mobile/android/base/resources/menu-v11/preferences_search_menu.xml deleted file mode 100644 index 9a3cdc72a88..00000000000 --- a/mobile/android/base/resources/menu-v11/preferences_search_menu.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - diff --git a/mobile/android/base/resources/xml-v11/preferences_search.xml b/mobile/android/base/resources/xml-v11/preferences_search.xml deleted file mode 100644 index cda787acf0b..00000000000 --- a/mobile/android/base/resources/xml-v11/preferences_search.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/xml/preferences_search.xml b/mobile/android/base/resources/xml/preferences_search.xml index 0f594d3de5f..cda787acf0b 100644 --- a/mobile/android/base/resources/xml/preferences_search.xml +++ b/mobile/android/base/resources/xml/preferences_search.xml @@ -16,13 +16,6 @@ - - - - - - &pref_category_datareporting; &pref_category_installed_search_engines; &pref_category_add_search_providers; - &pref_category_search_restore_defaults; - &pref_search_restore_defaults; - &pref_search_restore_defaults_summary; &pref_search_tip; - &pref_category_devtools; &pref_developer_remotedebugging; &pref_developer_remotedebugging_docs; diff --git a/mobile/android/base/tests/testDistribution.java b/mobile/android/base/tests/testDistribution.java index 4b739a08aa7..0c2787ef1f8 100644 --- a/mobile/android/base/tests/testDistribution.java +++ b/mobile/android/base/tests/testDistribution.java @@ -159,7 +159,7 @@ public class testDistribution extends ContentProviderTest { private void checkSearchPlugin() { Actions.RepeatedEventExpecter eventExpecter = mActions.expectGeckoEvent("SearchEngines:Data"); - mActions.sendGeckoEvent("SearchEngines:GetVisible", null); + mActions.sendGeckoEvent("SearchEngines:Get", null); try { JSONObject data = new JSONObject(eventExpecter.blockForEventData()); diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index 6fccdff3a6e..6e6b640b19a 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -6608,10 +6608,10 @@ var SearchEngines = { 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:Remove", false); - Services.obs.addObserver(this, "SearchEngines:RestoreDefaults", false); Services.obs.addObserver(this, "SearchEngines:SetDefault", false); + Services.obs.addObserver(this, "SearchEngines:Remove", false); let filter = { matches: function (aElement) { @@ -6651,28 +6651,37 @@ var SearchEngines = { 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:Remove"); - Services.obs.removeObserver(this, "SearchEngines:RestoreDefaults"); Services.obs.removeObserver(this, "SearchEngines:SetDefault"); + Services.obs.removeObserver(this, "SearchEngines:Remove"); if (this._contextMenuId != null) NativeWindow.contextmenus.remove(this._contextMenuId); }, // Fetch list of search engines. all ? All engines : Visible engines only. - _handleSearchEnginesGetVisible: function _handleSearchEnginesGetVisible(rv, all) { + _handleSearchEnginesGet: function _handleSearchEnginesGet(rv, all) { if (!Components.isSuccessCode(rv)) { Cu.reportError("Could not initialize search service, bailing out."); return; } + let engineData; + if (all) { + engineData = Services.search.getEngines({}); + } else { + engineData = Services.search.getVisibleEngines({}); + } + + // These engines are the bundled ones - they may not be uninstalled. + let immutableEngines = Services.search.getDefaultEngines(); - let engineData = Services.search.getVisibleEngines({}); let searchEngines = engineData.map(function (engine) { return { name: engine.name, identifier: engine.identifier, iconURI: (engine.iconURI ? engine.iconURI.spec : null), - hidden: engine.hidden + hidden: engine.hidden, + immutable: immutableEngines.indexOf(engine) != -1 }; }); @@ -6709,6 +6718,13 @@ var SearchEngines = { } catch (e) {} }, + _handleSearchEnginesGetAll: function _handleSearchEnginesGetAll(rv) { + this._handleSearchEnginesGet(rv, true); + }, + _handleSearchEnginesGetVisible: function _handleSearchEnginesGetVisible(rv) { + this._handleSearchEnginesGet(rv, false) + }, + // Helper method to extract the engine name from a JSON. Simplifies the observe function. _extractEngineFromJSON: function _extractEngineFromJSON(aData) { let data = JSON.parse(aData); @@ -6724,16 +6740,9 @@ var SearchEngines = { case "SearchEngines:GetVisible": Services.search.init(this._handleSearchEnginesGetVisible.bind(this)); break; - case "SearchEngines:Remove": - // Make sure the engine isn't hidden before removing it, to make sure it's - // visible if the user later re-adds it (works around bug 341833) - engine = this._extractEngineFromJSON(aData); - engine.hidden = false; - Services.search.removeEngine(engine); - break; - case "SearchEngines:RestoreDefaults": - // Un-hides all default engines. - Services.search.restoreDefaultEngines(); + case "SearchEngines:Get": + // Return a list of all engines, including "Hidden" ones. + Services.search.init(this._handleSearchEnginesGetAll.bind(this)); break; case "SearchEngines:SetDefault": engine = this._extractEngineFromJSON(aData); @@ -6741,7 +6750,13 @@ var SearchEngines = { Services.search.moveEngine(engine, 0); Services.search.defaultEngine = engine; break; - + case "SearchEngines:Remove": + // Make sure the engine isn't hidden before removing it, to make sure it's + // visible if the user later re-adds it (works around bug 341833) + engine = this._extractEngineFromJSON(aData); + engine.hidden = false; + Services.search.removeEngine(engine); + break; default: dump("Unexpected message type observed: " + aTopic); break; From 57962dec10d45b8b0760696e99917749c7516b65 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 19 Dec 2013 17:31:05 -0600 Subject: [PATCH 5/8] Bug 946813 - Part 1: Expose invisibleToDebugger as a sandbox flag. r=bholley --HG-- extra : rebase_source : 7d4963efc10423bb3e451da0eb1f6a03d7531831 --- js/xpconnect/src/Sandbox.cpp | 4 ++++ js/xpconnect/src/xpcprivate.h | 2 ++ .../tests/chrome/test_evalInSandbox.xul | 21 +++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/js/xpconnect/src/Sandbox.cpp b/js/xpconnect/src/Sandbox.cpp index 098833cae34..2a1b9e5993e 100644 --- a/js/xpconnect/src/Sandbox.cpp +++ b/js/xpconnect/src/Sandbox.cpp @@ -1053,6 +1053,9 @@ xpc::CreateSandboxObject(JSContext *cx, MutableHandleValue vp, nsISupports *prin compartmentOptions.setSameZoneAs(js::UncheckedUnwrap(options.sameZoneAs)); else compartmentOptions.setZone(JS::SystemZone); + + compartmentOptions.setInvisibleToDebugger(options.invisibleToDebugger); + RootedObject sandbox(cx, xpc::CreateGlobalObject(cx, &SandboxClass, principal, compartmentOptions)); if (!sandbox) @@ -1486,6 +1489,7 @@ SandboxOptions::Parse() ParseBoolean("wantExportHelpers", &wantExportHelpers) && ParseString("sandboxName", sandboxName) && ParseObject("sameZoneAs", &sameZoneAs) && + ParseBoolean("invisibleToDebugger", &invisibleToDebugger) && ParseGlobalProperties() && ParseValue("metadata", &metadata); } diff --git a/js/xpconnect/src/xpcprivate.h b/js/xpconnect/src/xpcprivate.h index fc8264b2fd3..7d551e06055 100644 --- a/js/xpconnect/src/xpcprivate.h +++ b/js/xpconnect/src/xpcprivate.h @@ -3425,6 +3425,7 @@ public: , wantExportHelpers(false) , proto(cx) , sameZoneAs(cx) + , invisibleToDebugger(false) , metadata(cx) { } @@ -3436,6 +3437,7 @@ public: JS::RootedObject proto; nsCString sandboxName; JS::RootedObject sameZoneAs; + bool invisibleToDebugger; GlobalProperties globalProperties; JS::RootedValue metadata; diff --git a/js/xpconnect/tests/chrome/test_evalInSandbox.xul b/js/xpconnect/tests/chrome/test_evalInSandbox.xul index 65c5651466c..75607cabce1 100644 --- a/js/xpconnect/tests/chrome/test_evalInSandbox.xul +++ b/js/xpconnect/tests/chrome/test_evalInSandbox.xul @@ -150,5 +150,26 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=533596 catch (e) { ok(false, "sameZoneAs works"); } + + Cu.import("resource://gre/modules/jsdebugger.jsm"); + addDebuggerToGlobal(this); + + try { + let dbg = new Debugger(); + let sandbox = new Cu.Sandbox(this, { invisibleToDebugger: false }); + dbg.addDebuggee(sandbox); + ok(true, "debugger added visible value"); + } catch(e) { + ok(false, "debugger could not add visible value"); + } + + try { + let dbg = new Debugger(); + let sandbox = new Cu.Sandbox(this, { invisibleToDebugger: true }); + dbg.addDebuggee(sandbox); + ok(false, "debugger added invisible value"); + } catch(e) { + ok(true, "debugger did not add invisible value"); + } ]]> From a105908434ee23fe8af4cc23915ee7c2acf56df7 Mon Sep 17 00:00:00 2001 From: Scott Johnson Date: Tue, 1 Oct 2013 14:52:13 -0500 Subject: [PATCH 6/8] Bug 878935, Part 2: Pause painting while reflow-on-zoom is in progress to provide a better user experience. [r=kats,dbaron] --- docshell/base/nsIMarkupDocumentViewer.idl | 12 ++ layout/base/nsDocumentViewer.cpp | 42 ++++++- layout/base/nsIPresShell.h | 21 +++- layout/base/nsPresShell.cpp | 29 +++++ layout/base/nsPresShell.h | 3 + mobile/android/chrome/content/browser.js | 130 ++++++++++++++++------ 6 files changed, 197 insertions(+), 40 deletions(-) diff --git a/docshell/base/nsIMarkupDocumentViewer.idl b/docshell/base/nsIMarkupDocumentViewer.idl index 342f741435c..27eef2b6261 100644 --- a/docshell/base/nsIMarkupDocumentViewer.idl +++ b/docshell/base/nsIMarkupDocumentViewer.idl @@ -82,6 +82,18 @@ interface nsIMarkupDocumentViewer : nsISupports */ void changeMaxLineBoxWidth(in int32_t maxLineBoxWidth); + /** + * Instruct the refresh driver to discontinue painting until further + * notice. + */ + void pausePainting(); + + /** + * Instruct the refresh driver to resume painting after a previous call to + * pausePainting(). + */ + void resumePainting(); + /* * Render the document as if being viewed on a device with the specified * media type. This will cause a reflow. diff --git a/layout/base/nsDocumentViewer.cpp b/layout/base/nsDocumentViewer.cpp index 885c8444d2d..ee428a0919f 100644 --- a/layout/base/nsDocumentViewer.cpp +++ b/layout/base/nsDocumentViewer.cpp @@ -2700,6 +2700,17 @@ struct LineBoxInfo nscoord mMaxLineBoxWidth; }; +static void +ChangeChildPaintingEnabled(nsIMarkupDocumentViewer* aChild, void* aClosure) +{ + bool* enablePainting = (bool*) aClosure; + if (*enablePainting) { + aChild->ResumePainting(); + } else { + aChild->PausePainting(); + } +} + static void ChangeChildMaxLineBoxWidth(nsIMarkupDocumentViewer* aChild, void* aClosure) { @@ -3126,7 +3137,36 @@ NS_IMETHODIMP nsDocumentViewer::AppendSubtree(nsTArrayPausePainting(); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocumentViewer::ResumePainting() +{ + bool enablePaint = true; + CallChildren(ChangeChildPaintingEnabled, &enablePaint); + + nsIPresShell* presShell = GetPresShell(); + if (presShell) { + presShell->ResumePainting(); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocumentViewer::ChangeMaxLineBoxWidth(int32_t aMaxLineBoxWidth) { // Change the max line box width for all children. struct LineBoxInfo lbi = { aMaxLineBoxWidth }; diff --git a/layout/base/nsIPresShell.h b/layout/base/nsIPresShell.h index 30d4f602516..db9470f7b92 100644 --- a/layout/base/nsIPresShell.h +++ b/layout/base/nsIPresShell.h @@ -129,10 +129,10 @@ typedef struct CapturingContentInfo { } CapturingContentInfo; -// f5b542a9-eaf0-4560-a656-37a9d379864c +// 0e4f2b36-7ab8-43c5-b912-5c311566297c #define NS_IPRESSHELL_IID \ -{ 0xf5b542a9, 0xeaf0, 0x4560, \ - { 0x37, 0xa9, 0xd3, 0x79, 0x86, 0x4c } } +{ 0xde498c49, 0xf83f, 0x47bf, \ + {0x8c, 0xc6, 0x8f, 0xf8, 0x74, 0x62, 0x22, 0x23 } } // debug VerifyReflow flags #define VERIFY_REFLOW_ON 0x01 @@ -836,6 +836,20 @@ public: */ bool IsPaintingSuppressed() const { return mPaintingSuppressed; } + /** + * Pause painting by freezing the refresh driver of this and all parent + * presentations. This may not have the desired effect if this pres shell + * has its own refresh driver. + */ + virtual void PausePainting() = 0; + + /** + * Resume painting by thawing the refresh driver of this and all parent + * presentations. This may not have the desired effect if this pres shell + * has its own refresh driver. + */ + virtual void ResumePainting() = 0; + /** * Unsuppress painting. */ @@ -1601,6 +1615,7 @@ protected: bool mFontSizeInflationForceEnabled; bool mFontSizeInflationDisabledInMasterProcess; bool mFontSizeInflationEnabled; + bool mPaintingIsFrozen; // Dirty bit indicating that mFontSizeInflationEnabled needs to be recomputed. bool mFontSizeInflationEnabledIsDirty; diff --git a/layout/base/nsPresShell.cpp b/layout/base/nsPresShell.cpp index a600299c2b9..b578f6d1670 100644 --- a/layout/base/nsPresShell.cpp +++ b/layout/base/nsPresShell.cpp @@ -724,6 +724,8 @@ PresShell::PresShell() "layout.reflow.synthMouseMove", true); addedSynthMouseMove = true; } + + mPaintingIsFrozen = false; } NS_IMPL_ISUPPORTS7(PresShell, nsIPresShell, nsIDocumentObserver, @@ -744,6 +746,13 @@ PresShell::~PresShell() mLastCallbackEventRequest == nullptr, "post-reflow queues not empty. This means we're leaking"); + // Verify that if painting was frozen, but we're being removed from the tree, + // that we now re-enable painting on our refresh driver, since it may need to + // be re-used by another presentation. + if (mPaintingIsFrozen) { + mPresContext->RefreshDriver()->Thaw(); + } + #ifdef DEBUG MOZ_ASSERT(mPresArenaAllocCount == 0, "Some pres arena objects were not freed"); @@ -9931,3 +9940,23 @@ nsIPresShell::SetMaxLineBoxWidth(nscoord aMaxLineBoxWidth) FrameNeedsReflow(GetRootFrame(), eResize, NS_FRAME_HAS_DIRTY_CHILDREN); } } + +void +PresShell::PausePainting() +{ + if (GetPresContext()->RefreshDriver()->PresContext() != GetPresContext()) + return; + + mPaintingIsFrozen = true; + GetPresContext()->RefreshDriver()->Freeze(); +} + +void +PresShell::ResumePainting() +{ + if (GetPresContext()->RefreshDriver()->PresContext() != GetPresContext()) + return; + + mPaintingIsFrozen = false; + GetPresContext()->RefreshDriver()->Thaw(); +} diff --git a/layout/base/nsPresShell.h b/layout/base/nsPresShell.h index 8e14e4d8eef..932f9662bb7 100644 --- a/layout/base/nsPresShell.h +++ b/layout/base/nsPresShell.h @@ -689,6 +689,9 @@ protected: virtual void ThemeChanged() MOZ_OVERRIDE { mPresContext->ThemeChanged(); } virtual void BackingScaleFactorChanged() MOZ_OVERRIDE { mPresContext->UIResolutionChanged(); } + virtual void PausePainting() MOZ_OVERRIDE; + virtual void ResumePainting() MOZ_OVERRIDE; + void UpdateImageVisibility(); nsRevocableEventPtr > mUpdateImageVisibilityEvent; diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index 6e6b640b19a..32e4b872992 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -182,6 +182,11 @@ function doChangeMaxLineBoxWidth(aWidth) { if (range) { BrowserEventHandler._zoomInAndSnapToRange(range); + } else { + // In this case, we actually didn't zoom into a specific range. It probably + // happened from a page load reflow-on-zoom event, so we need to make sure + // painting is re-enabled. + BrowserApp.selectedTab.clearReflowOnZoomPendingActions(); } } @@ -2744,6 +2749,7 @@ Tab.prototype = { this.browser.addEventListener("MozApplicationManifest", this, true); Services.obs.addObserver(this, "before-first-paint", false); + Services.obs.addObserver(this, "after-viewport-change", false); Services.prefs.addObserver("browser.ui.zoom.force-user-scalable", this, false); if (aParams.delayLoad) { @@ -2803,21 +2809,58 @@ Tab.prototype = { return minFontSize / this.getInflatedFontSizeFor(aElement); }, + clearReflowOnZoomPendingActions: function() { + // Reflow was completed, so now re-enable painting. + let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation); + let docShell = webNav.QueryInterface(Ci.nsIDocShell); + let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer); + docViewer.resumePainting(); + + BrowserApp.selectedTab._mReflozPositioned = false; + }, + + /** + * Reflow on zoom consists of a few different sub-operations: + * + * 1. When a double-tap event is seen, we verify that the correct preferences + * are enabled and perform the pre-position handling calculation. We also + * signal that reflow-on-zoom should be performed at this time, and pause + * painting. + * 2. During the next call to setViewport(), which is in the Tab prototype, + * we detect that a call to changeMaxLineBoxWidth should be performed. If + * we're zooming out, then the max line box width should be reset at this + * time. Otherwise, we call performReflowOnZoom. + * 2a. PerformReflowOnZoom() and resetMaxLineBoxWidth() schedule a call to + * doChangeMaxLineBoxWidth, based on a timeout specified in preferences. + * 3. doChangeMaxLineBoxWidth changes the line box width (which also + * schedules a reflow event), and then calls _zoomInAndSnapToRange. + * 4. _zoomInAndSnapToRange performs the positioning of reflow-on-zoom and + * then re-enables painting. + * + * Some of the events happen synchronously, while others happen asynchronously. + * The following is a rough sketch of the progression of events: + * + * double tap event seen -> onDoubleTap() -> ... asynchronous ... + * -> setViewport() -> performReflowOnZoom() -> ... asynchronous ... + * -> doChangeMaxLineBoxWidth() -> _zoomInAndSnapToRange() + * -> ... asynchronous ... -> setViewport() -> Observe('after-viewport-change') + * -> resumePainting() + */ performReflowOnZoom: function(aViewport) { - let zoom = this._drawZoom ? this._drawZoom : aViewport.zoom; + let zoom = this._drawZoom ? this._drawZoom : aViewport.zoom; - let viewportWidth = gScreenWidth / zoom; - let reflozTimeout = Services.prefs.getIntPref("browser.zoom.reflowZoom.reflowTimeout"); + let viewportWidth = gScreenWidth / zoom; + let reflozTimeout = Services.prefs.getIntPref("browser.zoom.reflowZoom.reflowTimeout"); - if (gReflowPending) { - clearTimeout(gReflowPending); - } + if (gReflowPending) { + clearTimeout(gReflowPending); + } - // We add in a bit of fudge just so that the end characters - // don't accidentally get clipped. 15px is an arbitrary choice. - gReflowPending = setTimeout(doChangeMaxLineBoxWidth, - reflozTimeout, - viewportWidth - 15); + // We add in a bit of fudge just so that the end characters + // don't accidentally get clipped. 15px is an arbitrary choice. + gReflowPending = setTimeout(doChangeMaxLineBoxWidth, + reflozTimeout, + viewportWidth - 15); }, /** @@ -2889,6 +2932,7 @@ Tab.prototype = { this.browser.removeEventListener("MozApplicationManifest", this, true); Services.obs.removeObserver(this, "before-first-paint"); + Services.obs.removeObserver(this, "after-viewport-change"); Services.prefs.removeObserver("browser.ui.zoom.force-user-scalable", this); // Make sure the previously selected panel remains selected. The selected panel of a deck is @@ -3175,6 +3219,7 @@ Tab.prototype = { // In this case, the user pinch-zoomed in, so we don't want to // preserve position as we would with reflow-on-zoom. BrowserApp.selectedTab.probablyNeedRefloz = false; + BrowserApp.selectedTab.clearReflowOnZoomPendingActions(); BrowserApp.selectedTab._mReflozPoint = null; } @@ -4166,6 +4211,11 @@ Tab.prototype = { BrowserApp.selectedTab.performReflowOnZoom(vp); } break; + case "after-viewport-change": + if (BrowserApp.selectedTab._mReflozPositioned) { + BrowserApp.selectedTab.clearReflowOnZoomPendingActions(); + } + break; case "nsPref:changed": if (aData == "browser.ui.zoom.force-user-scalable") ViewportHandler.updateMetadata(this, false); @@ -4484,13 +4534,23 @@ var BrowserEventHandler = { if (BrowserEventHandler.mReflozPref && !BrowserApp.selectedTab._mReflozPoint && !this._shouldSuppressReflowOnZoom(element)) { - let data = JSON.parse(aData); - let zoomPointX = data.x; - let zoomPointY = data.y; - BrowserApp.selectedTab._mReflozPoint = { x: zoomPointX, y: zoomPointY, - range: BrowserApp.selectedBrowser.contentDocument.caretPositionFromPoint(zoomPointX, zoomPointY) }; - BrowserApp.selectedTab.probablyNeedRefloz = true; + // See comment above performReflowOnZoom() for a detailed description of + // the events happening in the reflow-on-zoom operation. + let data = JSON.parse(aData); + let zoomPointX = data.x; + let zoomPointY = data.y; + + BrowserApp.selectedTab._mReflozPoint = { x: zoomPointX, y: zoomPointY, + range: BrowserApp.selectedBrowser.contentDocument.caretPositionFromPoint(zoomPointX, zoomPointY) }; + + // Before we perform a reflow on zoom, let's disable painting. + let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation); + let docShell = webNav.QueryInterface(Ci.nsIDocShell); + let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer); + docViewer.pausePainting(); + + BrowserApp.selectedTab.probablyNeedRefloz = true; } if (!element) { @@ -4590,11 +4650,7 @@ var BrowserEventHandler = { }, _zoomInAndSnapToRange: function(aRange) { - if (!aRange) { - Cu.reportError("aRange is null in zoomInAndSnapToRange. Unable to maintain position."); - return; - } - + // aRange is always non-null here, since a check happened previously. let viewport = BrowserApp.selectedTab.getViewport(); let fudge = 15; // Add a bit of fudge. let boundingElement = aRange.offsetNode; @@ -4617,6 +4673,8 @@ var BrowserEventHandler = { let leftAdjustment = parseInt(boundingStyle.paddingLeft) + parseInt(boundingStyle.borderLeftWidth); + BrowserApp.selectedTab._mReflozPositioned = true; + rect.type = "Browser:ZoomToRect"; rect.x = Math.max(viewport.cssPageLeft, rect.x - fudge + leftAdjustment); rect.y = Math.max(topPos, viewport.cssPageTop); @@ -4625,22 +4683,22 @@ var BrowserEventHandler = { sendMessageToJava(rect); BrowserApp.selectedTab._mReflozPoint = null; - }, + }, - onPinchFinish: function(aData) { - let data = {}; - try { - data = JSON.parse(aData); - } catch(ex) { - console.log(ex); - return; - } + onPinchFinish: function(aData) { + let data = {}; + try { + data = JSON.parse(aData); + } catch(ex) { + console.log(ex); + return; + } - if (BrowserEventHandler.mReflozPref && - data.zoomDelta < 0.0) { - BrowserEventHandler.resetMaxLineBoxWidth(); - } - }, + if (BrowserEventHandler.mReflozPref && + data.zoomDelta < 0.0) { + BrowserEventHandler.resetMaxLineBoxWidth(); + } + }, _shouldZoomToElement: function(aElement) { let win = aElement.ownerDocument.defaultView; From 8f547383485ed44f5e6eb168877604d608f933c8 Mon Sep 17 00:00:00 2001 From: Michael Comella Date: Thu, 19 Dec 2013 19:49:02 -0800 Subject: [PATCH 7/8] Bug 946656 - Correct regex that determines a loading page in WaitHelper. r=margaret --- mobile/android/base/tests/helpers/WaitHelper.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mobile/android/base/tests/helpers/WaitHelper.java b/mobile/android/base/tests/helpers/WaitHelper.java index 41ef3498913..ad7967a05ca 100644 --- a/mobile/android/base/tests/helpers/WaitHelper.java +++ b/mobile/android/base/tests/helpers/WaitHelper.java @@ -15,6 +15,8 @@ import org.mozilla.gecko.tests.UITestContext.ComponentType; import com.jayway.android.robotium.solo.Condition; import com.jayway.android.robotium.solo.Solo; +import java.util.regex.Pattern; + /** * Provides functionality related to waiting on certain events to happen. */ @@ -131,7 +133,7 @@ public final class WaitHelper { ToolbarTitleTextChangeVerifier.class.getSimpleName() + ": "; // A regex that matches the page title that shows up while the page is loading. - private static final String LOADING_REGEX = "^[A-Za-z]{3,9}://"; + private static final Pattern LOADING_PREFIX = Pattern.compile("[A-Za-z]{3,9}://"); private CharSequence mOldTitleText; @@ -157,7 +159,7 @@ public final class WaitHelper { // (e.g. the page title). However, the title is set to the URL before the title is // loaded from the server and set as the final page title; we ignore the // intermediate URL loading state here. - final boolean isLoading = title.toString().matches(LOADING_REGEX); + final boolean isLoading = LOADING_PREFIX.matcher(title).lookingAt(); final boolean hasStateChanged = !isLoading && !mOldTitleText.equals(title); if (hasStateChanged) { From 058bac8da4939f33d6b487101c65d8cb1bfbf372 Mon Sep 17 00:00:00 2001 From: Brad Lassey Date: Thu, 12 Dec 2013 01:43:44 -0500 Subject: [PATCH 8/8] bug 868341 - pinch to zoom jumps all over page randomly r=mfinkle,kats --- .../base/gfx/JavaPanZoomController.java | 39 ++++++++++++------- mobile/android/chrome/content/browser.js | 32 +++++++++++---- 2 files changed, 49 insertions(+), 22 deletions(-) diff --git a/mobile/android/base/gfx/JavaPanZoomController.java b/mobile/android/base/gfx/JavaPanZoomController.java index f233516034e..95673aee07c 100644 --- a/mobile/android/base/gfx/JavaPanZoomController.java +++ b/mobile/android/base/gfx/JavaPanZoomController.java @@ -245,12 +245,16 @@ class JavaPanZoomController final RectF zoomRect = new RectF(x, y, x + (float)message.getDouble("w"), y + (float)message.getDouble("h")); - mTarget.post(new Runnable() { - @Override - public void run() { - animatedZoomTo(zoomRect); - } - }); + if (message.optBoolean("animate", true)) { + mTarget.post(new Runnable() { + @Override + public void run() { + animatedZoomTo(zoomRect); + } + }); + } else { + mTarget.setViewportMetrics(getMetricsToZoomTo(zoomRect)); + } } else if (MESSAGE_ZOOM_PAGE.equals(event)) { ImmutableViewportMetrics metrics = getMetrics(); RectF cssPageRect = metrics.getCssPageRect(); @@ -264,12 +268,16 @@ class JavaPanZoomController y + dh/2, cssPageRect.width(), y + dh/2 + newHeight); - mTarget.post(new Runnable() { - @Override - public void run() { - animatedZoomTo(r); - } - }); + if (message.optBoolean("animate", true)) { + mTarget.post(new Runnable() { + @Override + public void run() { + animatedZoomTo(r); + } + }); + } else { + mTarget.setViewportMetrics(getMetricsToZoomTo(r)); + } } else if (MESSAGE_TOUCH_LISTENER.equals(event)) { int tabId = message.getInt("tabID"); final Tab tab = Tabs.getInstance().getTab(tabId); @@ -1399,7 +1407,7 @@ class JavaPanZoomController * While we usually use device pixels, @zoomToRect must be specified in CSS * pixels. */ - private boolean animatedZoomTo(RectF zoomToRect) { + private ImmutableViewportMetrics getMetricsToZoomTo(RectF zoomToRect) { final float startZoom = getMetrics().zoomFactor; RectF viewport = getMetrics().getViewport(); @@ -1434,8 +1442,11 @@ class JavaPanZoomController // 2. now run getValidViewportMetrics on it, so that the target viewport is // clamped down to prevent overscroll, over-zoom, and other bad conditions. finalMetrics = getValidViewportMetrics(finalMetrics); + return finalMetrics; + } - bounce(finalMetrics, PanZoomState.ANIMATED_ZOOM); + private boolean animatedZoomTo(RectF zoomToRect) { + bounce(getMetricsToZoomTo(zoomToRect), PanZoomState.ANIMATED_ZOOM); return true; } diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index 32e4b872992..e9c897a8d39 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -178,15 +178,20 @@ function doChangeMaxLineBoxWidth(aWidth) { range = BrowserApp.selectedTab._mReflozPoint.range; } - docViewer.changeMaxLineBoxWidth(aWidth); + try { + docViewer.pausePainting(); + docViewer.changeMaxLineBoxWidth(aWidth); - if (range) { - BrowserEventHandler._zoomInAndSnapToRange(range); - } else { - // In this case, we actually didn't zoom into a specific range. It probably - // happened from a page load reflow-on-zoom event, so we need to make sure - // painting is re-enabled. - BrowserApp.selectedTab.clearReflowOnZoomPendingActions(); + if (range) { + BrowserEventHandler._zoomInAndSnapToRange(range); + } else { + // In this case, we actually didn't zoom into a specific range. It + // probably happened from a page load reflow-on-zoom event, so we + // need to make sure painting is re-enabled. + BrowserApp.selectedTab.clearReflowOnZoomPendingActions(); + } + } finally { + docViewer.resumePainting(); } } @@ -3223,10 +3228,17 @@ Tab.prototype = { BrowserApp.selectedTab._mReflozPoint = null; } + let docViewer = null; + if (isZooming && BrowserEventHandler.mReflozPref && BrowserApp.selectedTab._mReflozPoint && BrowserApp.selectedTab.probablyNeedRefloz) { + let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation); + let docShell = webNav.QueryInterface(Ci.nsIDocShell); + docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer); + docViewer.pausePainting(); + BrowserApp.selectedTab.performReflowOnZoom(aViewport); BrowserApp.selectedTab.probablyNeedRefloz = false; } @@ -3255,6 +3267,9 @@ Tab.prototype = { aViewport.fixedMarginLeft / aViewport.zoom); Services.obs.notifyObservers(null, "after-viewport-change", ""); + if (docViewer) { + docViewer.resumePainting(); + } }, setResolution: function(aZoom, aForce) { @@ -4680,6 +4695,7 @@ var BrowserEventHandler = { rect.y = Math.max(topPos, viewport.cssPageTop); rect.w = viewport.cssWidth; rect.h = viewport.cssHeight; + rect.animate = false; sendMessageToJava(rect); BrowserApp.selectedTab._mReflozPoint = null;