import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -0,0 +1,304 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.search;
import android.net.Uri;
import android.util.Log;
import android.util.Xml;
import org.mozilla.gecko.util.StringUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/**
* Extend this class to add a new search engine to
* the search activity.
*/
public class SearchEngine {
private static final String LOG_TAG = "SearchEngine";
private static final String URLTYPE_SUGGEST_JSON = "application/x-suggestions+json";
private static final String URLTYPE_SEARCH_HTML = "text/html";
private static final String URL_REL_MOBILE = "mobile";
// Parameters copied from nsSearchService.js
private static final String MOZ_PARAM_LOCALE = "\\{moz:locale\\}";
private static final String MOZ_PARAM_DIST_ID = "\\{moz:distributionID\\}";
private static final String MOZ_PARAM_OFFICIAL = "\\{moz:official\\}";
// Supported OpenSearch parameters
// See http://opensearch.a9.com/spec/1.1/querysyntax/#core
private static final String OS_PARAM_USER_DEFINED = "\\{searchTerms\\??\\}";
private static final String OS_PARAM_INPUT_ENCODING = "\\{inputEncoding\\??\\}";
private static final String OS_PARAM_LANGUAGE = "\\{language\\??\\}";
private static final String OS_PARAM_OUTPUT_ENCODING = "\\{outputEncoding\\??\\}";
private static final String OS_PARAM_OPTIONAL = "\\{(?:\\w+:)?\\w+\\?\\}";
// Boilerplate bookmarklet-style JS for injecting CSS into the
// head of a web page. The actual CSS is inserted at `%s`.
private static final String STYLE_INJECTION_SCRIPT =
"javascript:(function(){" +
"var tag=document.createElement('style');" +
"tag.type='text/css';" +
"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;
// Ordered list of preferred results URIs.
private final List<Uri> resultsUris = new ArrayList<Uri>();
private Uri suggestUri;
/**
*
* @param in InputStream of open search plugin XML
*/
public SearchEngine(String identifier, InputStream in) throws IOException, XmlPullParserException {
this.identifier = identifier;
final XmlPullParser parser = Xml.newPullParser();
parser.setInput(in, null);
parser.nextTag();
readSearchPlugin(parser);
}
private void readSearchPlugin(XmlPullParser parser) throws XmlPullParserException, IOException {
if (XmlPullParser.START_TAG != parser.getEventType()) {
throw new XmlPullParserException("Expected start tag: " + parser.getPositionDescription());
}
final String name = parser.getName();
if (!"SearchPlugin".equals(name) && !"OpenSearchDescription".equals(name)) {
throw new XmlPullParserException("Expected <SearchPlugin> or <OpenSearchDescription> as root tag: "
+ parser.getPositionDescription());
}
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
final String tag = parser.getName();
if (tag.equals("ShortName")) {
readShortName(parser);
} else if (tag.equals("Url")) {
readUrl(parser);
} else if (tag.equals("Image")) {
readImage(parser);
} else {
skip(parser);
}
}
}
private void readShortName(XmlPullParser parser) throws IOException, XmlPullParserException {
parser.require(XmlPullParser.START_TAG, null, "ShortName");
if (parser.next() == XmlPullParser.TEXT) {
shortName = parser.getText();
parser.nextTag();
}
}
private void readUrl(XmlPullParser parser) throws XmlPullParserException, IOException {
parser.require(XmlPullParser.START_TAG, null, "Url");
final String type = parser.getAttributeValue(null, "type");
final String template = parser.getAttributeValue(null, "template");
final String rel = parser.getAttributeValue(null, "rel");
Uri uri = Uri.parse(template);
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
final String tag = parser.getName();
if (tag.equals("Param")) {
final String name = parser.getAttributeValue(null, "name");
final String value = parser.getAttributeValue(null, "value");
uri = uri.buildUpon().appendQueryParameter(name, value).build();
parser.nextTag();
// TODO: Support for other tags
//} else if (tag.equals("MozParam")) {
} else {
skip(parser);
}
}
if (type.equals(URLTYPE_SEARCH_HTML)) {
// Prefer mobile URIs.
if (rel != null && rel.equals(URL_REL_MOBILE)) {
resultsUris.add(0, uri);
} else {
resultsUris.add(uri);
}
} else if (type.equals(URLTYPE_SUGGEST_JSON)) {
suggestUri = uri;
}
}
private void readImage(XmlPullParser parser) throws XmlPullParserException, IOException {
parser.require(XmlPullParser.START_TAG, null, "Image");
// TODO: Use width and height to get a preferred icon URL.
//final int width = Integer.parseInt(parser.getAttributeValue(null, "width"));
//final int height = Integer.parseInt(parser.getAttributeValue(null, "height"));
if (parser.next() == XmlPullParser.TEXT) {
iconURL = parser.getText();
parser.nextTag();
}
}
private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
if (parser.getEventType() != XmlPullParser.START_TAG) {
throw new IllegalStateException();
}
int depth = 1;
while (depth != 0) {
switch (parser.next()) {
case XmlPullParser.END_TAG:
depth--;
break;
case XmlPullParser.START_TAG:
depth++;
break;
}
}
}
/**
* HACKS! We'll need to replace this with endpoints that return the correct content.
*
* Retrieve a JS snippet, in bookmarklet style, that can be used
* to modify the results page.
*/
public String getInjectableJs() {
final String css;
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}";
} else if (identifier.equals("yahoo")) {
css = "#nav,#header{display:none}";
} else {
css = "";
}
return String.format(STYLE_INJECTION_SCRIPT, css);
}
public String getIdentifier() {
return identifier;
}
public String getName() {
return shortName;
}
public String getIconURL() {
return iconURL;
}
/**
* Finds the search query encoded in a given results URL.
*
* @param url Current results URL.
* @return The search query, or an empty string if a query couldn't be found.
*/
public String queryForResultsUrl(String url) {
final Uri resultsUri = getResultsUri();
final Set<String> names = StringUtils.getQueryParameterNames(resultsUri);
for (String name : names) {
if (resultsUri.getQueryParameter(name).matches(OS_PARAM_USER_DEFINED)) {
return Uri.parse(url).getQueryParameter(name);
}
}
return "";
}
/**
* Create a uri string that can be used to fetch the results page.
*
* @param query The user's query. This method will escape and encode the query.
*/
public String resultsUriForQuery(String query) {
final Uri resultsUri = getResultsUri();
if (resultsUri == null) {
Log.e(LOG_TAG, "No results URL for search engine: " + shortName);
return "";
}
final String template = Uri.decode(resultsUri.toString());
return paramSubstitution(template, Uri.encode(query));
}
/**
* Create a uri string to fetch autocomplete suggestions.
*
* @param query The user's query. This method will escape and encode the query.
*/
public String getSuggestionTemplate(String query) {
if (suggestUri == null) {
Log.e(LOG_TAG, "No suggestions template for search engine: " + shortName);
return "";
}
final String template = Uri.decode(suggestUri.toString());
return paramSubstitution(template, Uri.encode(query));
}
/**
* @return Preferred results URI.
*/
private Uri getResultsUri() {
if (resultsUris.isEmpty()) {
return null;
}
return resultsUris.get(0);
}
/**
* Formats template string with proper parameters. Modeled after
* ParamSubstitution in nsSearchService.js
*
* @param template
* @param query
* @return
*/
private String paramSubstitution(String template, String query) {
final String locale = Locale.getDefault().toString();
template = template.replaceAll(MOZ_PARAM_LOCALE, locale);
template = template.replaceAll(MOZ_PARAM_DIST_ID, "");
template = template.replaceAll(MOZ_PARAM_OFFICIAL, "unofficial");
template = template.replaceAll(OS_PARAM_USER_DEFINED, query);
template = template.replaceAll(OS_PARAM_INPUT_ENCODING, "UTF-8");
template = template.replaceAll(OS_PARAM_LANGUAGE, locale);
template = template.replaceAll(OS_PARAM_OUTPUT_ENCODING, "UTF-8");
// Replace any optional parameters
template = template.replaceAll(OS_PARAM_OPTIONAL, "");
return template;
}
}

View file

@ -0,0 +1,764 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.search;
import android.content.Context;
import android.content.SharedPreferences;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.annotation.WorkerThread;
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.GeckoProfile;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.R;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.util.FileUtils;
import org.mozilla.gecko.util.GeckoJarReader;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.util.RawResource;
import org.mozilla.gecko.util.ThreadUtils;
import org.xmlpull.v1.XmlPullParserException;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.ref.WeakReference;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Locale;
/**
* This class is not thread-safe, except where otherwise noted.
*
* This class contains a reference to {@link Context} - DO NOT LEAK!
*/
public class SearchEngineManager implements SharedPreferences.OnSharedPreferenceChangeListener {
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";
// Gecko pref that defines the name of the default searchplugin locale.
private static final String PREF_GECKO_DEFAULT_LOCALE = "distribution.searchplugins.defaultLocale";
// Key for shared preference that stores default engine name.
private static final String PREF_DEFAULT_ENGINE_KEY = "search.engines.defaultname";
// Key for shared preference that stores search region.
private static final String PREF_REGION_KEY = "search.region";
// URL for the geo-ip location service. Keep in sync with "browser.search.geoip.url" perference in Gecko.
private static final String GEOIP_LOCATION_URL = "https://location.services.mozilla.com/v1/country?key=" + AppConstants.MOZ_MOZILLA_API_KEY;
// This should go through GeckoInterface to get the UA, but the search activity
// doesn't use a GeckoView yet. Until it does, get the UA directly.
private static final String USER_AGENT = HardwareUtils.isTablet() ?
AppConstants.USER_AGENT_FENNEC_TABLET : AppConstants.USER_AGENT_FENNEC_MOBILE;
private final Context context;
private final Distribution distribution;
@Nullable private volatile SearchEngineCallback changeCallback;
@Nullable private volatile SearchEngine engine;
// Cached version of default locale included in Gecko chrome manifest.
// This should only be accessed from the background thread.
private String fallbackLocale;
// Cached version of default locale included in Distribution preferences.
// This should only be accessed from the background thread.
private String distributionLocale;
public static interface SearchEngineCallback {
public void execute(@Nullable SearchEngine engine);
}
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. This can be called from any thread.
*
* @param changeCallback 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;
}
/**
* Perform an action with the user's default search engine. This can be called from any thread.
*
* @param callback The callback to be used with the user's default search engine. The call
* may be sync or async; if the call is async, it will be called on the
* ui thread.
*/
public void getEngine(SearchEngineCallback callback) {
if (engine != null) {
callback.execute(engine);
} else {
getDefaultEngine(callback);
}
}
/**
* Should be called when the object goes out of scope.
*/
public void unregisterListeners() {
GeckoSharedPrefs.forApp(context).unregisterOnSharedPreferenceChangeListener(this);
}
private volatile int ignorePreferenceChange = 0;
@UiThread // according to the docs.
@Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
if (!TextUtils.equals(PREF_DEFAULT_ENGINE_KEY, key)) {
return;
}
if (ignorePreferenceChange > 0) {
ignorePreferenceChange--;
return;
}
getDefaultEngine(changeCallback);
}
/**
* Runs a SearchEngineCallback on the main thread.
*/
private void runCallback(final SearchEngine engine, @Nullable final SearchEngineCallback callback) {
ThreadUtils.postToUiThread(new RunCallbackUiThreadRunnable(this, engine, callback));
}
// Static is not strictly necessary but the outer class has a reference to Context so we should GC ASAP.
private static class RunCallbackUiThreadRunnable implements Runnable {
private final WeakReference<SearchEngineManager> searchEngineManagerWeakReference;
private final SearchEngine searchEngine;
private final SearchEngineCallback callback;
public RunCallbackUiThreadRunnable(final SearchEngineManager searchEngineManager, final SearchEngine searchEngine,
final SearchEngineCallback callback) {
this.searchEngineManagerWeakReference = new WeakReference<>(searchEngineManager);
this.searchEngine = searchEngine;
this.callback = callback;
}
@UiThread
@Override
public void run() {
final SearchEngineManager searchEngineManager = searchEngineManagerWeakReference.get();
if (searchEngineManager == null) {
return;
}
// Cache engine for future calls to getEngine.
searchEngineManager.engine = searchEngine;
if (callback != null) {
callback.execute(searchEngine);
}
}
}
/**
* 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.
*
* 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 getDefaultEngine(final SearchEngineCallback callback) {
// This runnable is posted to the background thread.
distribution.addOnDistributionReadyCallback(new GetDefaultEngineDistributionCallbacks(this, callback));
}
// Static is not strictly necessary but the outer class contains a reference to Context so we should GC ASAP.
private static class GetDefaultEngineDistributionCallbacks implements Distribution.ReadyCallback {
private final WeakReference<SearchEngineManager> searchEngineManagerWeakReference;
private final SearchEngineCallback callback;
public GetDefaultEngineDistributionCallbacks(final SearchEngineManager searchEngineManager,
final SearchEngineCallback callback) {
this.searchEngineManagerWeakReference = new WeakReference<>(searchEngineManager);
this.callback = callback;
}
@Override
public void distributionNotFound() {
defaultBehavior();
}
@Override
public void distributionFound(Distribution distribution) {
defaultBehavior();
}
@Override
public void distributionArrivedLate(Distribution distribution) {
final SearchEngineManager searchEngineManager = searchEngineManagerWeakReference.get();
if (searchEngineManager == null) {
return;
}
// Let's see if there's a name in the distro.
// If so, just this once we'll override the saved value.
final String name = searchEngineManager.getDefaultEngineNameFromDistribution();
if (name == null) {
return;
}
// Store the default engine name for the future.
// Increment an 'ignore' counter so that this preference change
// won't cause getDefaultEngine to be called again.
searchEngineManager.ignorePreferenceChange++;
GeckoSharedPrefs.forApp(searchEngineManager.context)
.edit()
.putString(PREF_DEFAULT_ENGINE_KEY, name)
.apply();
final SearchEngine engine = searchEngineManager.createEngineFromName(name);
searchEngineManager.runCallback(engine, callback);
}
@WorkerThread // calling methods are @WorkerThread
private void defaultBehavior() {
final SearchEngineManager searchEngineManager = searchEngineManagerWeakReference.get();
if (searchEngineManager == null) {
return;
}
// First look for a default name stored in shared preferences.
String name = GeckoSharedPrefs.forApp(searchEngineManager.context).getString(PREF_DEFAULT_ENGINE_KEY, null);
// Check for a region stored in shared preferences. If we don't have a region,
// we should force a recheck of the default engine.
String region = GeckoSharedPrefs.forApp(searchEngineManager.context).getString(PREF_REGION_KEY, null);
if (name != null && region != null) {
Log.d(LOG_TAG, "Found default engine name in SharedPreferences: " + name);
} else {
// First, look for the default search engine in a distribution.
name = searchEngineManager.getDefaultEngineNameFromDistribution();
if (name == null) {
// Otherwise, get the default engine that we ship.
name = searchEngineManager.getDefaultEngineNameFromLocale();
}
// Store the default engine name for the future.
// Increment an 'ignore' counter so that this preference change
// won't cause getDefaultEngine to be called again.
searchEngineManager.ignorePreferenceChange++;
GeckoSharedPrefs.forApp(searchEngineManager.context)
.edit()
.putString(PREF_DEFAULT_ENGINE_KEY, name)
.apply();
}
final SearchEngine engine = searchEngineManager.createEngineFromName(name);
searchEngineManager.runCallback(engine, callback);
}
}
/**
* Looks for a default search engine included in a distribution.
* This method must be called after the distribution is ready.
*
* @return search engine name.
*/
private String getDefaultEngineNameFromDistribution() {
if (!distribution.exists()) {
return null;
}
final File prefFile = distribution.getDistributionFile("preferences.json");
if (prefFile == null) {
return null;
}
try {
final JSONObject all = FileUtils.readJSONObjectFromFile(prefFile);
// First, look for a default locale specified by the distribution.
if (all.has("Preferences")) {
final JSONObject prefs = all.getJSONObject("Preferences");
if (prefs.has(PREF_GECKO_DEFAULT_LOCALE)) {
Log.d(LOG_TAG, "Found default searchplugin locale in distribution Preferences.");
distributionLocale = prefs.getString(PREF_GECKO_DEFAULT_LOCALE);
}
}
// Then, check to see if there's a locale-specific default engine override.
final String languageTag = Locales.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 engine 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;
}
/**
* Helper function for converting an InputStream to a String.
* @param is InputStream you want to convert to a String
*
* @return String containing the data
*/
private String getHttpResponse(HttpURLConnection conn) {
InputStream is = null;
try {
is = new BufferedInputStream(conn.getInputStream());
return new java.util.Scanner(is).useDelimiter("\\A").next();
} catch (Exception e) {
return "";
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
Log.e(LOG_TAG, "Error closing InputStream", e);
}
}
}
}
/**
* Gets the country code based on the current IP, using the Mozilla Location Service.
* We cache the country code in a shared preference, so we only fetch from the network
* once.
*
* @return String containing the country code
*/
private String fetchCountryCode() {
// First, we look to see if we have a cached code.
final String region = GeckoSharedPrefs.forApp(context).getString(PREF_REGION_KEY, null);
if (region != null) {
return region;
}
// Since we didn't have a cached code, we need to fetch a code from the service.
try {
String responseText = null;
URL url = new URL(GEOIP_LOCATION_URL);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
try {
// POST an empty JSON object.
final String message = "{}";
urlConnection.setDoOutput(true);
urlConnection.setConnectTimeout(10000);
urlConnection.setReadTimeout(10000);
urlConnection.setRequestMethod("POST");
urlConnection.setRequestProperty("User-Agent", USER_AGENT);
urlConnection.setRequestProperty("Content-Type", "application/json");
urlConnection.setFixedLengthStreamingMode(message.getBytes().length);
final OutputStream out = urlConnection.getOutputStream();
out.write(message.getBytes());
out.close();
responseText = getHttpResponse(urlConnection);
} finally {
urlConnection.disconnect();
}
if (responseText == null) {
Log.e(LOG_TAG, "Country code fetch failed");
return null;
}
// Extract the country code and save it for later in a cache.
final JSONObject response = new JSONObject(responseText);
return response.optString("country_code", null);
} catch (Exception e) {
Log.e(LOG_TAG, "Country code fetch failed", 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));
// Get the region used to fence search engines.
String region = fetchCountryCode();
// Store the result, even if it's empty. If we fail to get a region, we never
// try to get it again, and we will always fallback to the non-region engine.
GeckoSharedPrefs.forApp(context)
.edit()
.putString(PREF_REGION_KEY, (region == null ? "" : region))
.apply();
if (region != null) {
if (browsersearch.has("regions")) {
final JSONObject regions = browsersearch.getJSONObject("regions");
if (regions.has(region)) {
final JSONObject regionData = regions.getJSONObject(region);
Log.d(LOG_TAG, "Found region-specific default engine name in browsersearch.json.");
return regionData.getString("default");
}
}
}
// Either we have no geoip region, or we didn't find the right region and we are falling back to the default.
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;
}
// Collect an array of files to scan using the same approach as
// DirectoryService._appendDistroSearchDirs which states:
// Common engines are loaded for all locales. If there is no locale directory for
// the current locale, there is a pref: "distribution.searchplugins.defaultLocale",
// which specifies a default locale to use.
ArrayList<File> files = new ArrayList<>();
// Load files from the common folder first
final File[] commonFiles = (new File(pluginsDir, "common")).listFiles();
if (commonFiles != null) {
Collections.addAll(files, commonFiles);
}
// Next, check to see if there's a locale-specific override.
final File localeDir = new File(pluginsDir, "locale");
if (localeDir != null) {
final String languageTag = Locales.getLanguageTag(Locale.getDefault());
final File[] localeFiles = (new File(localeDir, languageTag)).listFiles();
if (localeFiles != null) {
Collections.addAll(files, localeFiles);
} else {
// We didn't append the locale dir - try the default one.
if (distributionLocale != null) {
final File[] defaultLocaleFiles = (new File(localeDir, distributionLocale)).listFiles();
if (defaultLocaleFiles != null) {
Collections.addAll(files, defaultLocaleFiles);
}
}
}
}
if (files.isEmpty()) {
Log.e(LOG_TAG, "Could not find search plugin files in distribution directory");
return null;
}
return createEngineFromFileList(files.toArray(new File[files.size()]), 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");
if (in == null) {
return null;
}
final BufferedReader br = getBufferedReader(in);
try {
String identifier;
while ((identifier = br.readLine()) != null) {
final InputStream pluginIn = getInputStreamFromSearchPluginsJar(identifier + ".xml");
// pluginIn can be null if the xml file doesn't exist which
// can happen with :hidden plugins
if (pluginIn != null) {
final SearchEngine engine = createEngineFromInputStream(identifier, pluginIn);
if (engine != null && engine.getName().equals(name)) {
return engine;
}
}
}
} catch (Exception e) {
Log.e(LOG_TAG, "Error creating shipped search engine from name: " + name, e);
} finally {
try {
br.close();
} catch (IOException e) {
// Ignore.
}
}
return null;
}
/**
* Creates a SearchEngine instance for a search plugin in the profile directory.
*
* 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 createEngineFromProfile(String name) {
final File pluginsDir = GeckoProfile.get(context).getFile("searchplugins");
if (pluginsDir == null) {
return null;
}
final File[] files = pluginsDir.listFiles();
if (files == null) {
Log.e(LOG_TAG, "Could not find search plugin files in profile directory");
return null;
}
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. Should not be null.
* @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 search 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);
} finally {
in.close();
}
} catch (Exception e) {
Log.e(LOG_TAG, "Exception creating search engine", e);
}
return null;
}
/**
* Reads a file from the searchplugins directory in the Gecko jar.
*
* @param fileName name of the file to read.
* @return InputStream for file.
*/
private InputStream getInputStreamFromSearchPluginsJar(String fileName) {
final Locale locale = Locale.getDefault();
// First, try a file path for the full locale.
final String languageTag = Locales.getLanguageTag(locale);
String url = getSearchPluginsJarURL(context, languageTag, fileName);
InputStream in = GeckoJarReader.getStream(context, url);
if (in != null) {
return in;
}
// If that doesn't work, try a file path for just the language.
final String language = Locales.getLanguage(locale);
if (!languageTag.equals(language)) {
url = getSearchPluginsJarURL(context, language, fileName);
in = GeckoJarReader.getStream(context, url);
if (in != null) {
return in;
}
}
// Finally, fall back to default locale defined in chrome registry.
url = getSearchPluginsJarURL(context, getFallbackLocale(), fileName);
return GeckoJarReader.getStream(context, url);
}
/**
* Finds a fallback locale in the Gecko chrome registry. If a locale is declared
* here, we should be guaranteed to find a searchplugins directory for it.
*
* This method should only be accessed from the background thread.
*/
private String getFallbackLocale() {
if (fallbackLocale != null) {
return fallbackLocale;
}
final InputStream in = GeckoJarReader.getStream(
context, GeckoJarReader.getJarURL(context, "chrome/chrome.manifest"));
if (in == null) {
return null;
}
final BufferedReader br = getBufferedReader(in);
try {
String line;
while ((line = br.readLine()) != null) {
// We're looking for a line like "locale global en-US en-US/locale/en-US/global/"
// https://developer.mozilla.org/en/docs/Chrome_Registration#locale
if (line.startsWith("locale global ")) {
fallbackLocale = line.split(" ", 4)[2];
break;
}
}
} catch (IOException e) {
Log.e(LOG_TAG, "Error reading fallback locale from chrome registry", e);
} finally {
try {
br.close();
} catch (IOException e) {
// Ignore.
}
}
return fallbackLocale;
}
/**
* Gets the jar URL for a file in the searchplugins directory.
*
* @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 static String getSearchPluginsJarURL(Context context, String locale, String fileName) {
final String path = "chrome/" + locale + "/locale/" + locale + "/browser/searchplugins/" + fileName;
return GeckoJarReader.getJarURL(context, path);
}
private BufferedReader getBufferedReader(InputStream in) {
try {
return new BufferedReader(new InputStreamReader(in, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// Cannot happen.
return null;
}
}
}