Bug 1091728 - Use correct gecko default search engine in search activity. r=rnewman

This commit is contained in:
Margaret Leibovic 2014-11-13 14:32:03 -08:00
Родитель 9bb10ffa42
Коммит 611d572f7f
5 изменённых файлов: 298 добавлений и 127 удалений

Просмотреть файл

@ -6920,7 +6920,7 @@ var SearchEngines = {
PREF_SUGGEST_PROMPTED: "browser.search.suggest.prompted",
// Shared preference key used for search activity default engine.
PREF_SEARCH_ACTIVITY_ENGINE_KEY: "search.engines.default",
PREF_SEARCH_ACTIVITY_ENGINE_KEY: "search.engines.defaultname",
init: function init() {
Services.obs.addObserver(this, "SearchEngines:Add", false);
@ -7060,34 +7060,7 @@ var SearchEngines = {
// Updates the search activity pref when the default engine changes.
_setSearchActivityDefaultPref: function _setSearchActivityDefaultPref(engine) {
// Helper function copied from nsSearchService.js. This is the logic that is used
// to create file names for search plugin XML serialized to disk.
function sanitizeName(aName) {
const maxLength = 60;
const minLength = 1;
let name = aName.toLowerCase();
name = name.replace(/\s+/g, "-");
name = name.replace(/[^-a-z0-9]/g, "");
if (name.length < minLength) {
// Well, in this case, we're kinda screwed. In this case, the search service
// generates a random file name, so to do this the right way, we'd need
// to open up search.json and see what file name is stored.
Cu.reportError("Couldn't create search plugin file name from engine name: " + aName);
return null;
}
// Force max length.
return name.substring(0, maxLength);
}
let identifier = engine.identifier;
if (identifier === null) {
// The identifier will be null for non-built-in engines. In this case, we need to
// figure out an identifier to store from the engine name.
identifier = sanitizeName(engine.name);
}
SharedPreferences.forApp().setCharPref(this.PREF_SEARCH_ACTIVITY_ENGINE_KEY, identifier);
SharedPreferences.forApp().setCharPref(this.PREF_SEARCH_ACTIVITY_ENGINE_KEY, engine.name);
},
// Display context menu listing names of the search engines available to be added.

Просмотреть файл

@ -17,9 +17,4 @@ package org.mozilla.search;
public class Constants {
public static final String ABOUT_BLANK = "about:blank";
// TODO: Localize this with region.properties (or a similar solution). See bug 1065306.
public static final String DEFAULT_ENGINE_IDENTIFIER = "yahoo";
public static final String PREF_SEARCH_ENGINE_KEY = "search.engines.default";
}

Просмотреть файл

@ -10,6 +10,7 @@ import org.mozilla.gecko.R;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.db.BrowserContract.SearchHistory;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.health.BrowserHealthRecorder;
import org.mozilla.search.autocomplete.SearchBar;
import org.mozilla.search.autocomplete.SuggestionsFragment;
@ -101,7 +102,7 @@ public class SearchActivity extends LocaleAware.LocaleAwareFragmentActivity
suggestionsFragment = (SuggestionsFragment) getSupportFragmentManager().findFragmentById(R.id.suggestions);
postSearchFragment = (PostSearchFragment) getSupportFragmentManager().findFragmentById(R.id.postsearch);
searchEngineManager = new SearchEngineManager(this);
searchEngineManager = new SearchEngineManager(this, Distribution.init(this));
searchEngineManager.setChangeCallback(this);
// Initialize the fragments with the selected search engine.
@ -279,7 +280,10 @@ public class SearchActivity extends LocaleAware.LocaleAwareFragmentActivity
searchEngineManager.getEngine(new SearchEngineCallback() {
@Override
public void execute(SearchEngine engine) {
postSearchFragment.startSearch(engine, query);
// TODO: If engine is null, we should show an error message.
if (engine != null) {
postSearchFragment.startSearch(engine, query);
}
}
});
}
@ -293,6 +297,10 @@ public class SearchActivity extends LocaleAware.LocaleAwareFragmentActivity
*/
@Override
public void execute(SearchEngine engine) {
// TODO: If engine is null, we should show an error message.
if (engine == null) {
return;
}
this.engine = engine;
suggestionsFragment.setEngine(engine);
searchBar.setEngine(engine);

Просмотреть файл

@ -52,7 +52,9 @@ public class SearchEngine {
"document.getElementsByTagName('head')[0].appendChild(tag);" +
"tag.innerText='%s'})();";
// The Gecko search identifier. This will be null for engines that don't ship with the locale.
private final String identifier;
private String shortName;
private String iconURL;
@ -189,7 +191,9 @@ public class SearchEngine {
public String getInjectableJs() {
final String css;
if (identifier.equals("bing")) {
if (identifier == null) {
css = "";
} else if (identifier.equals("bing")) {
css = "#mHeader{display:none}#contentWrapper{margin-top:0}";
} else if (identifier.equals("google")) {
css = "#sfcnt,#top_nav{display:none}";
@ -247,7 +251,7 @@ public class SearchEngine {
public String resultsUriForQuery(String query) {
final Uri resultsUri = getResultsUri();
if (resultsUri == null) {
Log.e(LOG_TAG, "No results URL for search engine: " + identifier);
Log.e(LOG_TAG, "No results URL for search engine: " + shortName);
return "";
}
final String template = Uri.decode(resultsUri.toString());
@ -261,7 +265,7 @@ public class SearchEngine {
*/
public String getSuggestionTemplate(String query) {
if (suggestUri == null) {
Log.e(LOG_TAG, "No suggestions template for search engine: " + identifier);
Log.e(LOG_TAG, "No suggestions template for search engine: " + shortName);
return "";
}
final String template = Uri.decode(suggestUri.toString());

Просмотреть файл

@ -6,15 +6,21 @@ package org.mozilla.search.providers;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.text.TextUtils;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.BrowserLocaleManager;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.R;
import org.mozilla.gecko.util.FileUtils;
import org.mozilla.gecko.util.GeckoJarReader;
import org.mozilla.gecko.util.RawResource;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.search.Constants;
import org.xmlpull.v1.XmlPullParserException;
@ -30,9 +36,16 @@ import java.util.List;
import java.util.Locale;
public class SearchEngineManager implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String LOG_TAG = "SearchEngineManager";
private static final String LOG_TAG = "GeckoSearchEngineManager";
// Gecko pref that defines the name of the default search engine.
private static final String PREF_GECKO_DEFAULT_ENGINE = "browser.search.defaultenginename";
// Key for shared preference that stores default engine name.
private static final String PREF_DEFAULT_ENGINE_KEY = "search.engines.defaultname";
private Context context;
private Distribution distribution;
private SearchEngineCallback changeCallback;
private SearchEngine engine;
@ -40,11 +53,19 @@ public class SearchEngineManager implements SharedPreferences.OnSharedPreference
public void execute(SearchEngine engine);
}
public SearchEngineManager(Context context) {
public SearchEngineManager(Context context, Distribution distribution) {
this.context = context;
this.distribution = distribution;
GeckoSharedPrefs.forApp(context).registerOnSharedPreferenceChangeListener(this);
}
/**
* Sets a callback to be called when the default engine changes.
*
* @param callback SearchEngineCallback to be called after the search engine
* changed. This will run on the UI thread.
* Note: callback may be called with null engine.
*/
public void setChangeCallback(SearchEngineCallback changeCallback) {
this.changeCallback = changeCallback;
}
@ -60,80 +81,230 @@ public class SearchEngineManager implements SharedPreferences.OnSharedPreference
if (engine != null) {
callback.execute(engine);
} else {
getEngineFromPrefs(callback);
getDefaultEngine(callback);
}
}
public void destroy() {
GeckoSharedPrefs.forApp(context).unregisterOnSharedPreferenceChangeListener(this);
context = null;
distribution = null;
changeCallback = null;
engine = null;
}
private int ignorePreferenceChange = 0;
@Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
if (!TextUtils.equals(Constants.PREF_SEARCH_ENGINE_KEY, key)) {
if (!TextUtils.equals(PREF_DEFAULT_ENGINE_KEY, key)) {
return;
}
getEngineFromPrefs(changeCallback);
if (ignorePreferenceChange > 0) {
ignorePreferenceChange--;
return;
}
getDefaultEngine(changeCallback);
}
/**
* Look up the current search engine in shared preferences.
* Creates a SearchEngine instance and caches it for use on the main thread.
* Runs a SearchEngineCallback on the main thread.
*/
private void runCallback(final SearchEngine engine, final SearchEngineCallback callback) {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
// Cache engine for future calls to getEngine.
SearchEngineManager.this.engine = engine;
callback.execute(engine);
}
});
}
/**
* This method finds and creates the default search engine. It will first look for
* the default engine name, then create the engine from that name.
*
* @param callback a SearchEngineCallback to be called after successfully looking
* To find the default engine name, we first look in shared preferences, then
* the distribution (if one exists), and finally fall back to the localized default.
*
* @param callback SearchEngineCallback to be called after successfully looking
* up the search engine. This will run on the UI thread.
* Note: callback may be called with null engine.
*/
private void getEngineFromPrefs(final SearchEngineCallback callback) {
final AsyncTask<Void, Void, SearchEngine> task = new AsyncTask<Void, Void, SearchEngine>() {
private void getDefaultEngine(final SearchEngineCallback callback) {
// This runnable is posted to the background thread.
distribution.addOnDistributionReadyCallback(new Runnable() {
@Override
protected SearchEngine doInBackground(Void... params) {
String identifier = GeckoSharedPrefs.forApp(context).getString(Constants.PREF_SEARCH_ENGINE_KEY, null);
if (!TextUtils.isEmpty(identifier)) {
try {
return createEngine(identifier);
} catch (IllegalArgumentException e) {
Log.e(LOG_TAG, "Exception creating search engine from pref. Falling back to default engine.", e);
public void run() {
// First look for a default name stored in shared preferences.
String name = GeckoSharedPrefs.forApp(context).getString(PREF_DEFAULT_ENGINE_KEY, null);
if (name != null) {
Log.d(LOG_TAG, "Found default engine name in SharedPreferences: " + name);
} else {
// First, look for the default search engine in a distribution.
name = getDefaultEngineNameFromDistribution();
if (name == null) {
// Otherwise, get the default engine that we ship.
name = getDefaultEngineNameFromLocale();
}
// Store the default engine name for the future.
// Increment an 'ignore' counter so that this preference change
// won'tcause getDefaultEngine to be called again.
ignorePreferenceChange++;
GeckoSharedPrefs.forApp(context)
.edit()
.putString(PREF_DEFAULT_ENGINE_KEY, name)
.apply();
}
try {
return createEngine(Constants.DEFAULT_ENGINE_IDENTIFIER);
} catch (IllegalArgumentException e) {
Log.e(LOG_TAG, "Exception creating search engine from default identifier. " +
"This will happen if the locale doesn't contain the default search plugin.", e);
}
return null;
final SearchEngine engine = createEngineFromName(name);
runCallback(engine, callback);
}
@Override
protected void onPostExecute(SearchEngine engine) {
if (engine != null) {
// Only touch engine on the main thread.
SearchEngineManager.this.engine = engine;
if (callback != null) {
callback.execute(engine);
}
}
}
};
task.execute();
});
}
/**
* Creates a list of SearchEngine instances from all available open search plugins.
* This method does disk I/O, call it from a background thread.
* Looks for a default search engine included in a distribution.
* This method must be called after the distribution is ready.
*
* @return List of SearchEngine instances
* @return search engine name.
*/
public List<SearchEngine> getAllEngines() {
// First try to read the engine list from the jar.
InputStream in = getInputStreamFromJar("list.txt");
private String getDefaultEngineNameFromDistribution() {
if (!distribution.exists()) {
return null;
}
final List<SearchEngine> list = new ArrayList<SearchEngine>();
final File prefFile = distribution.getDistributionFile("preferences.json");
if (prefFile == null) {
return null;
}
try {
final JSONObject all = new JSONObject(FileUtils.getFileContents(prefFile));
// First, check to see if there's a locale-specific override.
final String languageTag = BrowserLocaleManager.getLanguageTag(Locale.getDefault());
final String overridesKey = "LocalizablePreferences." + languageTag;
if (all.has(overridesKey)) {
final JSONObject overridePrefs = all.getJSONObject(overridesKey);
if (overridePrefs.has(PREF_GECKO_DEFAULT_ENGINE)) {
Log.d(LOG_TAG, "Found default engine name in distribution LocalizablePreferences override.");
return overridePrefs.getString(PREF_GECKO_DEFAULT_ENGINE);
}
}
// Next, check to see if there's a non-override default pref.
if (all.has("LocalizablePreferences")) {
final JSONObject localizablePrefs = all.getJSONObject("LocalizablePreferences");
if (localizablePrefs.has(PREF_GECKO_DEFAULT_ENGINE)) {
Log.d(LOG_TAG, "Found default engine name in distribution LocalizablePreferences.");
return localizablePrefs.getString(PREF_GECKO_DEFAULT_ENGINE);
}
}
} catch (IOException e) {
Log.e(LOG_TAG, "Error getting search engine name from preferences.json", e);
} catch (JSONException e) {
Log.e(LOG_TAG, "Error parsing preferences.json", e);
}
return null;
}
/**
* Looks for the default search engine shipped in the locale.
*
* @return search engine name.
*/
private String getDefaultEngineNameFromLocale() {
try {
final JSONObject browsersearch = new JSONObject(RawResource.getAsString(context, R.raw.browsersearch));
if (browsersearch.has("default")) {
Log.d(LOG_TAG, "Found default engine name in browsersearch.json.");
return browsersearch.getString("default");
}
} catch (IOException e) {
Log.e(LOG_TAG, "Error getting search engine name from browsersearch.json", e);
} catch (JSONException e) {
Log.e(LOG_TAG, "Error parsing browsersearch.json", e);
}
return null;
}
/**
* Creates a SearchEngine instance from an engine name.
*
* To create the engine, we first try to find the search plugin in the distribution
* (if one exists), followed by the localized plugins we ship with the browser, and
* then finally third-party plugins that are installed in the profile directory.
*
* This method must be called after the distribution is ready.
*
* @param name The search engine name (e.g. "Google" or "Amazon.com")
* @return SearchEngine instance for name.
*/
private SearchEngine createEngineFromName(String name) {
// First, look in the distribution.
SearchEngine engine = createEngineFromDistribution(name);
// Second, look in the jar for plugins shipped with the locale.
if (engine == null) {
engine = createEngineFromLocale(name);
}
// Finally, look in the profile for third-party plugins.
if (engine == null) {
engine = createEngineFromProfile(name);
}
if (engine == null) {
Log.e(LOG_TAG, "Could not create search engine from name: " + name);
}
return engine;
}
/**
* Creates a SearchEngine instance for a distribution search plugin.
*
* This method iterates through the distribution searchplugins directory,
* creating SearchEngine instances until it finds one with the right name.
*
* This method must be called after the distribution is ready.
*
* @param name Search engine name.
* @return SearchEngine instance for name.
*/
private SearchEngine createEngineFromDistribution(String name) {
if (!distribution.exists()) {
return null;
}
final File pluginsDir = distribution.getDistributionFile("searchplugins");
if (pluginsDir == null) {
return null;
}
final File[] files = (new File(pluginsDir, "common")).listFiles();
return createEngineFromFileList(files, name);
}
/**
* Creates a SearchEngine instance for a search plugin shipped in the locale.
*
* This method reads the list of search plugin file names from list.txt, then
* iterates through the files, creating SearchEngine instances until it finds one
* with the right name. Unfortunately, we need to do this because there is no
* other way to map the search engine "name" to the file for the search plugin.
*
* @param name Search engine name.
* @return SearchEngine instance for name.
*/
private SearchEngine createEngineFromLocale(String name) {
final InputStream in = getInputStreamFromSearchPluginsJar("list.txt");
InputStreamReader isr = null;
try {
@ -141,10 +312,14 @@ public class SearchEngineManager implements SharedPreferences.OnSharedPreference
BufferedReader br = new BufferedReader(isr);
String identifier;
while ((identifier = br.readLine()) != null) {
list.add(createEngine(identifier));
final InputStream pluginIn = getInputStreamFromSearchPluginsJar(identifier + ".xml");
final SearchEngine engine = createEngineFromInputStream(identifier, pluginIn);
if (engine != null && engine.getName().equals(name)) {
return engine;
}
}
} catch (IOException e) {
throw new IllegalStateException("Error creating all search engines from list.txt");
Log.e(LOG_TAG, "Error creating shipped search engine from name: " + name, e);
} finally {
if (isr != null) {
try {
@ -159,27 +334,62 @@ public class SearchEngineManager implements SharedPreferences.OnSharedPreference
// Ignore.
}
}
return list;
return null;
}
/**
* Creates a SearchEngine instance from an open search plugin.
* This method does disk I/O, call it from a background thread.
* Creates a SearchEngine instance for a search plugin in the profile directory.
*
* @param identifier search engine identifier (e.g. "google")
* @return SearchEngine instance for identifier
* This method iterates through the profile searchplugins directory, creating
* SearchEngine instances until it finds one with the right name.
*
* @param name Search engine name.
* @return SearchEngine instance for name.
*/
private SearchEngine createEngine(String identifier) {
InputStream in = getInputStreamFromJar(identifier + ".xml");
if (in == null) {
in = getEngineFromProfile(identifier);
private SearchEngine createEngineFromProfile(String name) {
final File pluginsDir = GeckoProfile.get(context).getFile("searchplugins");
if (pluginsDir == null) {
return null;
}
if (in == null) {
throw new IllegalArgumentException("Couldn't find search engine for identifier: " + identifier);
}
final File[] files = pluginsDir.listFiles();
return createEngineFromFileList(files, name);
}
/**
* This method iterates through an array of search plugin files, creating
* SearchEngine instances until it finds one with the right name.
*
* @param files Array of search plugin files.
* @param name Search engine name.
* @return SearchEngine instance for name.
*/
private SearchEngine createEngineFromFileList(File[] files, String name) {
for (int i = 0; i < files.length; i++) {
try {
final FileInputStream fis = new FileInputStream(files[i]);
final SearchEngine engine = createEngineFromInputStream(null, fis);
if (engine != null && engine.getName().equals(name)) {
return engine;
}
} catch (IOException e) {
Log.e(LOG_TAG, "Error creating earch engine from name: " + name, e);
}
}
return null;
}
/**
* Creates a SearchEngine instance from an InputStream.
*
* This method closes the stream after it is done reading it.
*
* @param identifier Seach engine identifier. This only exists for search engines that
* ship with the default set of engines in the locale.
* @param in InputStream for search plugin XML file.
* @return SearchEngine instance.
*/
private SearchEngine createEngineFromInputStream(String identifier, InputStream in) {
try {
try {
return new SearchEngine(identifier, in);
@ -194,13 +404,12 @@ public class SearchEngineManager implements SharedPreferences.OnSharedPreference
}
/**
* Reads a file from the searchplugins directory in the Gecko jar. This will only work
* if the search activity is built as part of mozilla-central.
* Reads a file from the searchplugins directory in the Gecko jar.
*
* @param fileName name of the file to read
* @return InputStream for file
* @param fileName name of the file to read.
* @return InputStream for file.
*/
private InputStream getInputStreamFromJar(String fileName) {
private InputStream getInputStreamFromSearchPluginsJar(String fileName) {
final Locale locale = Locale.getDefault();
// First, try a file path for the full locale.
@ -228,32 +437,14 @@ public class SearchEngineManager implements SharedPreferences.OnSharedPreference
}
/**
* Gets the jar URL for a file in the searchplugins directory
* Gets the jar URL for a file in the searchplugins directory.
*
* @param locale String representing the Gecko locale (e.g. "en-US")
* @param fileName name of the file to read
* @return URL for jar file
* @param locale String representing the Gecko locale (e.g. "en-US").
* @param fileName The name of the file to read.
* @return URL for jar file.
*/
private String getSearchPluginsJarURL(String locale, String fileName) {
final String path = "!/chrome/" + locale + "/locale/" + locale + "/browser/searchplugins/" + fileName;
return "jar:jar:file://" + context.getPackageResourcePath() + "!/" + AppConstants.OMNIJAR_NAME + path;
}
/**
* Opens the search plugin XML file from the searchplugins directory in the Gecko profile.
*
* @param identifier
* @return InputStream for search plugin file
*/
private InputStream getEngineFromProfile(String identifier) {
final File f = GeckoProfile.get(context).getFile("searchplugins/" + identifier + ".xml");
if (f.exists()) {
try {
return new FileInputStream(f);
} catch (FileNotFoundException e) {
Log.e(LOG_TAG, "Exception getting search engine from profile", e);
}
}
return null;
}
}