summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/search/SearchEngineManager.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/search/SearchEngineManager.java')
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/search/SearchEngineManager.java764
1 files changed, 764 insertions, 0 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/search/SearchEngineManager.java b/mobile/android/base/java/org/mozilla/gecko/search/SearchEngineManager.java
new file mode 100644
index 000000000..4b33db40a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/search/SearchEngineManager.java
@@ -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;
+ }
+ }
+}