Index: chrome/android/java/src/org/chromium/chrome/browser/physicalweb/PwsClientImpl.java |
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/physicalweb/PwsClientImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/physicalweb/PwsClientImpl.java |
index 20f0a611a287ed7e51c1a3c3d7a6c8507f0b204c..0bfcb4a01da80aa292cfc6b50e4f409bfd3bda14 100644 |
--- a/chrome/android/java/src/org/chromium/chrome/browser/physicalweb/PwsClientImpl.java |
+++ b/chrome/android/java/src/org/chromium/chrome/browser/physicalweb/PwsClientImpl.java |
@@ -4,12 +4,16 @@ |
package org.chromium.chrome.browser.physicalweb; |
+import android.content.Context; |
import android.graphics.Bitmap; |
import android.os.AsyncTask; |
+import android.os.Build; |
import org.chromium.base.Log; |
import org.chromium.base.ThreadUtils; |
+import org.chromium.base.VisibleForTesting; |
import org.chromium.chrome.GoogleAPIKeys; |
+import org.chromium.chrome.R; |
import org.chromium.chrome.browser.ChromeVersionInfo; |
import org.chromium.chrome.browser.physicalweb.PwsClient.FetchIconCallback; |
import org.chromium.chrome.browser.physicalweb.PwsClient.ResolveScanCallback; |
@@ -21,6 +25,8 @@ import org.json.JSONObject; |
import java.net.MalformedURLException; |
import java.util.ArrayList; |
import java.util.Collection; |
+import java.util.Formatter; |
+import java.util.Locale; |
/** |
* This class sends requests to the Physical Web Service. |
@@ -30,6 +36,25 @@ class PwsClientImpl implements PwsClient { |
private static final String ENDPOINT_URL = |
"https://physicalweb.googleapis.com/v1alpha1/urls:resolve"; |
+ // Format strings for creating the User-Agent string. It should somewhat resemble the Chrome for |
+ // Android User-Agent but doesn't need to match perfectly as this value will only be seen by the |
+ // Physical Web metadata service and favicon fetcher. |
+ // The WebKit version is not accessible from here so it is reported as 0.0. |
+ private static final String USER_AGENT_FORMAT = |
+ "Mozilla/5.0 (%s) AppleWebKit/0.0 (KHTML, like Gecko) %s Safari/0.0"; |
+ private static final String OS_INFO_FORMAT = "Linux; Android %s; %s Build/%s"; |
+ private static final String PRODUCT_FORMAT = "Chrome/%s Mobile"; |
+ |
+ private static String sUserAgent; |
nyquist
2016/05/25 22:02:48
These are lazily initialized, so could you mention
mattreynolds
2016/05/26 00:31:44
Done.
|
+ private static String sAcceptLanguage; |
+ private static String sDefaultLocale; |
+ |
+ private Context mContext; |
nyquist
2016/05/25 22:02:48
Could you add a comment as to the lifetime of this
mattreynolds
2016/05/26 00:31:44
Done. (also made it final)
|
+ |
+ public PwsClientImpl(Context context) { |
+ mContext = context; |
+ } |
+ |
private String getApiKey() { |
if (ChromeVersionInfo.isStableBuild()) { |
return GoogleAPIKeys.GOOGLE_API_KEY; |
@@ -117,7 +142,8 @@ class PwsClientImpl implements PwsClient { |
try { |
JSONObject payload = createResolveScanPayload(broadcastUrls); |
String url = ENDPOINT_URL + "?key=" + getApiKey(); |
- request = new JsonObjectHttpRequest(url, payload, requestCallback); |
+ request = new JsonObjectHttpRequest(url, getUserAgent(), getAcceptLanguage(), payload, |
+ requestCallback); |
} catch (MalformedURLException e) { |
Log.e(TAG, "Error creating PWS HTTP request", e); |
return; |
@@ -159,7 +185,8 @@ class PwsClientImpl implements PwsClient { |
// Create the request. |
BitmapHttpRequest request = null; |
try { |
- request = new BitmapHttpRequest(iconUrl, requestCallback); |
+ request = new BitmapHttpRequest(iconUrl, getUserAgent(), getAcceptLanguage(), |
+ requestCallback); |
} catch (MalformedURLException e) { |
Log.e(TAG, "Error creating icon request", e); |
return; |
@@ -167,4 +194,130 @@ class PwsClientImpl implements PwsClient { |
// The callback will be called on the main thread. |
AsyncTask.THREAD_POOL_EXECUTOR.execute(request); |
} |
+ |
+ /** |
+ * Recreate the Chrome for Android User-Agent string as closely as possible without calling any |
+ * native code. |
+ * @return A User-Agent string |
+ */ |
+ @VisibleForTesting |
+ String getUserAgent() { |
+ if (sUserAgent == null) { |
+ // Build the OS info string. |
+ // eg: Linux; Android 5.1.1; Nexus 4 Build/LMY48T |
+ String osInfo = String.format(OS_INFO_FORMAT, Build.VERSION.RELEASE, Build.MODEL, |
+ Build.ID); |
+ |
+ // Build the product string. |
+ // eg: Chrome/50.0.2661.89 Mobile |
+ String product = String.format(PRODUCT_FORMAT, ChromeVersionInfo.getProductVersion()); |
+ |
+ // Build the User-Agent string. |
+ // eg: Mozilla/5.0 (Linux; Android 5.1.1; Nexus 4 Build/LMY48T) AppleWebKit/0.0 (KHTML, |
+ // like Gecko) Chrome/50.0.2661.89 Mobile Safari/0.0 |
+ sUserAgent = String.format(USER_AGENT_FORMAT, osInfo, product); |
+ } |
+ return sUserAgent; |
+ } |
+ |
+ /** |
+ * Construct the Accept-Language string based on the current locale. |
+ * @return An Accept-Language string. |
+ */ |
+ @VisibleForTesting |
+ String getAcceptLanguage() { |
+ String defaultLocale = Locale.getDefault().toString(); |
+ if (sDefaultLocale == null || !sDefaultLocale.equals(defaultLocale)) { |
+ String acceptLanguages = mContext.getResources().getString(R.string.accept_languages); |
+ acceptLanguages = prependToAcceptLanguagesIfNecessary(defaultLocale, acceptLanguages); |
+ sAcceptLanguage = generateAcceptLanguageHeader(acceptLanguages); |
+ sDefaultLocale = defaultLocale; |
+ } |
+ return sAcceptLanguage; |
+ } |
+ |
+ /** |
+ * Handle the special cases in converting a language code/region code pair into an ISO-639-1 |
+ * language tag. |
+ * @param language The 2-character language code |
+ * @param region The 2-character country code |
+ * @return A language tag. |
+ */ |
+ @VisibleForTesting |
+ static String makeLanguageTag(String language, String region) { |
+ // Java mostly follows ISO-639-1 and ICU, except for the following three. |
+ // See documentation on java.util.Locale constructor for more. |
+ if ("iw".equals(language)) { |
+ language = "he"; |
nyquist
2016/05/25 22:02:48
It's a little bit awkward to reassign the input pa
mattreynolds
2016/05/26 00:31:44
Done.
|
+ } else if ("ji".equals(language)) { |
+ language = "yi"; |
+ } else if ("in".equals(language)) { |
+ language = "id"; |
+ } |
+ |
+ return language + "-" + region; |
+ } |
+ |
+ /** |
+ * Get the language code for the default locale and prepend it to the Accept-Language string if |
+ * it isn't already present. The logic should match PrependToAcceptLanguagesIfNecessary in |
+ * chrome/browser/android/preferences/pref_service_bridge.cc |
nyquist
2016/05/25 22:02:48
Should you update that code to refer to this code?
mattreynolds
2016/05/26 00:31:44
Done.
|
+ * @param locale A string representing the default locale. |
+ * @param acceptLanguages The default language list for the language of the user's locale. |
+ * @return An updated language list. |
+ */ |
+ @VisibleForTesting |
+ static String prependToAcceptLanguagesIfNecessary(String locale, String acceptLanguages) |
+ { |
+ if (locale.length() != 5 || locale.charAt(2) != '_') { |
+ return acceptLanguages; |
+ } |
+ |
+ String language = locale.substring(0, 2); |
+ String region = locale.substring(3); |
+ String languageTag = makeLanguageTag(language, region); |
+ |
+ if (!acceptLanguages.contains(languageTag)) { |
nyquist
2016/05/25 22:02:48
Could you flip this, and just return acceptLanguag
mattreynolds
2016/05/26 00:31:44
Done.
|
+ Formatter parts = new Formatter(); |
+ parts.format("%s,", languageTag); |
+ // If language is not in the accept languages list, also add language code. |
+ if (!acceptLanguages.contains(language + ",") && !acceptLanguages.endsWith(language)) { |
nyquist
2016/05/25 22:02:48
Is there ever any chance of this happening:
accep
mattreynolds
2016/05/26 00:31:44
It could fail to append the language tag correctly
|
+ parts.format("%s,", language); |
+ } |
+ parts.format("%s", acceptLanguages); |
+ return parts.toString(); |
+ } |
+ |
+ return acceptLanguages; |
+ } |
+ |
+ /** |
+ * Given a list of comma-delimited language codes, insert q-values to represent the relative |
+ * quality/precedence of each language. The logic should match GenerateAcceptLanguageHeader in |
+ * net/http/http_util.cc |
nyquist
2016/05/25 22:02:48
Do we want a back-reference?
mattreynolds
2016/05/26 00:31:44
Done.
|
+ * @param languageList A comma-delimited list of language codes containing no whitespace. |
+ * @return An Accept-Language header with q-values. |
+ */ |
+ @VisibleForTesting |
+ static String generateAcceptLanguageHeader(String languageList) { |
+ // We use integers for qvalue and qvalue decrement that are 10 times larger than actual |
+ // values to avoid a problem with comparing two floating point numbers. |
+ int kQvalueDecrement10 = 2; |
+ int qvalue10 = 10; |
+ String[] parts = languageList.split(","); |
+ Formatter langListWithQ = new Formatter(); |
+ for (String language : parts) { |
+ if (qvalue10 == 10) { |
+ // q=1.0 is implicit |
+ langListWithQ.format("%s", language); |
+ } else { |
+ langListWithQ.format(",%s;q=0.%d", language, qvalue10); |
+ } |
+ // It does not make sense to have 'q=0'. |
+ if (qvalue10 > kQvalueDecrement10) { |
+ qvalue10 -= kQvalueDecrement10; |
+ } |
+ } |
+ return langListWithQ.toString(); |
+ } |
} |