OLD | NEW |
1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 package org.chromium.components.variations.firstrun; | 5 package org.chromium.components.variations.firstrun; |
6 | 6 |
7 import android.content.Context; | |
8 import android.content.SharedPreferences; | 7 import android.content.SharedPreferences; |
9 import android.os.SystemClock; | 8 import android.os.SystemClock; |
10 | 9 |
11 import org.chromium.base.ContextUtils; | 10 import org.chromium.base.ContextUtils; |
12 import org.chromium.base.Log; | 11 import org.chromium.base.Log; |
13 import org.chromium.base.ThreadUtils; | 12 import org.chromium.base.ThreadUtils; |
14 import org.chromium.base.VisibleForTesting; | 13 import org.chromium.base.VisibleForTesting; |
15 import org.chromium.base.metrics.CachedMetrics.SparseHistogramSample; | 14 import org.chromium.base.metrics.CachedMetrics.SparseHistogramSample; |
16 import org.chromium.base.metrics.CachedMetrics.TimesHistogramSample; | 15 import org.chromium.base.metrics.CachedMetrics.TimesHistogramSample; |
17 | 16 |
18 import java.io.ByteArrayOutputStream; | 17 import java.io.ByteArrayOutputStream; |
19 import java.io.IOException; | 18 import java.io.IOException; |
20 import java.io.InputStream; | 19 import java.io.InputStream; |
21 import java.net.HttpURLConnection; | 20 import java.net.HttpURLConnection; |
22 import java.net.MalformedURLException; | 21 import java.net.MalformedURLException; |
23 import java.net.SocketTimeoutException; | 22 import java.net.SocketTimeoutException; |
24 import java.net.URL; | 23 import java.net.URL; |
25 import java.net.UnknownHostException; | 24 import java.net.UnknownHostException; |
26 import java.util.concurrent.TimeUnit; | 25 import java.util.concurrent.TimeUnit; |
27 | 26 |
28 /** | 27 /** |
29 * Fetches the variations seed before the actual first run of Chrome. | 28 * Fetches the variations seed before the actual first run of Chrome. |
30 */ | 29 */ |
31 public class VariationsSeedFetcher { | 30 public class VariationsSeedFetcher { |
32 private static final String TAG = "VariationsSeedFetch"; | 31 private static final String TAG = "VariationsSeedFetch"; |
| 32 |
| 33 public enum VariationsPlatform { ANDROID, ANDROID_WEBVIEW } |
| 34 |
33 private static final String VARIATIONS_SERVER_URL = | 35 private static final String VARIATIONS_SERVER_URL = |
34 "https://clientservices.googleapis.com/chrome-variations/seed?osname
=android"; | 36 "https://clientservices.googleapis.com/chrome-variations/seed?osname
="; |
35 | 37 |
36 private static final int BUFFER_SIZE = 4096; | 38 private static final int BUFFER_SIZE = 4096; |
37 private static final int READ_TIMEOUT = 3000; // time in ms | 39 private static final int READ_TIMEOUT = 3000; // time in ms |
38 private static final int REQUEST_TIMEOUT = 1000; // time in ms | 40 private static final int REQUEST_TIMEOUT = 1000; // time in ms |
39 | 41 |
40 // Values for the "Variations.FirstRun.SeedFetchResult" sparse histogram, wh
ich also logs | 42 // Values for the "Variations.FirstRun.SeedFetchResult" sparse histogram, wh
ich also logs |
41 // HTTP result codes. These are negative so that they don't conflict with th
e HTTP codes. | 43 // HTTP result codes. These are negative so that they don't conflict with th
e HTTP codes. |
42 // These values should not be renumbered or re-used since they are logged to
UMA. | 44 // These values should not be renumbered or re-used since they are logged to
UMA. |
43 private static final int SEED_FETCH_RESULT_UNKNOWN_HOST_EXCEPTION = -3; | 45 private static final int SEED_FETCH_RESULT_UNKNOWN_HOST_EXCEPTION = -3; |
44 private static final int SEED_FETCH_RESULT_TIMEOUT = -2; | 46 private static final int SEED_FETCH_RESULT_TIMEOUT = -2; |
45 private static final int SEED_FETCH_RESULT_IOEXCEPTION = -1; | 47 private static final int SEED_FETCH_RESULT_IOEXCEPTION = -1; |
46 | 48 |
47 @VisibleForTesting | 49 @VisibleForTesting |
48 static final String VARIATIONS_INITIALIZED_PREF = "variations_initialized"; | 50 static final String VARIATIONS_INITIALIZED_PREF = "variations_initialized"; |
49 | 51 |
50 // Synchronization lock | 52 // Synchronization lock to make singleton thread-safe. |
51 private static final Object sLock = new Object(); | 53 private static final Object sLock = new Object(); |
52 | 54 |
53 private static VariationsSeedFetcher sInstance; | 55 private static VariationsSeedFetcher sInstance; |
54 | 56 |
55 @VisibleForTesting | 57 @VisibleForTesting |
56 VariationsSeedFetcher() {} | 58 VariationsSeedFetcher() {} |
57 | 59 |
58 public static VariationsSeedFetcher get() { | 60 public static VariationsSeedFetcher get() { |
59 // TODO(aberent) Check not running on UI thread. Doing so however makes
Robolectric testing | 61 // TODO(aberent) Check not running on UI thread. Doing so however makes
Robolectric testing |
60 // of dependent classes difficult. | 62 // of dependent classes difficult. |
61 synchronized (sLock) { | 63 synchronized (sLock) { |
62 if (sInstance == null) { | 64 if (sInstance == null) { |
63 sInstance = new VariationsSeedFetcher(); | 65 sInstance = new VariationsSeedFetcher(); |
64 } | 66 } |
65 return sInstance; | 67 return sInstance; |
66 } | 68 } |
67 } | 69 } |
68 | 70 |
69 /** | 71 /** |
70 * Override the VariationsSeedFetcher, typically with a mock, for testing cl
asses that depend on | 72 * Override the VariationsSeedFetcher, typically with a mock, for testing cl
asses that depend on |
71 * this one. | 73 * this one. |
72 * @param fetcher the mock. | 74 * @param fetcher the mock. |
73 */ | 75 */ |
74 @VisibleForTesting | 76 @VisibleForTesting |
75 public static void setVariationsSeedFetcherForTesting(VariationsSeedFetcher
fetcher) { | 77 public static void setVariationsSeedFetcherForTesting(VariationsSeedFetcher
fetcher) { |
76 sInstance = fetcher; | 78 sInstance = fetcher; |
77 } | 79 } |
78 | 80 |
79 @VisibleForTesting | 81 @VisibleForTesting |
80 protected HttpURLConnection getServerConnection(String restrictMode) | 82 protected HttpURLConnection getServerConnection(VariationsPlatform platform, |
81 throws MalformedURLException, IOException { | 83 String restrictMode) throws MalformedURLException, IOException { |
82 String urlString = VARIATIONS_SERVER_URL; | 84 String urlString = VARIATIONS_SERVER_URL; |
| 85 switch (platform) { |
| 86 case ANDROID: |
| 87 urlString += "android"; |
| 88 break; |
| 89 case ANDROID_WEBVIEW: |
| 90 urlString += "android_webview"; |
| 91 break; |
| 92 default: |
| 93 assert false; |
| 94 } |
83 if (restrictMode != null && !restrictMode.isEmpty()) { | 95 if (restrictMode != null && !restrictMode.isEmpty()) { |
84 urlString += "&restrict=" + restrictMode; | 96 urlString += "&restrict=" + restrictMode; |
85 } | 97 } |
86 URL url = new URL(urlString); | 98 URL url = new URL(urlString); |
87 return (HttpURLConnection) url.openConnection(); | 99 return (HttpURLConnection) url.openConnection(); |
88 } | 100 } |
89 | 101 |
90 /** | 102 /** |
| 103 * Object holding the seed data and related fields retrieved from HTTP heade
rs. |
| 104 */ |
| 105 public static class SeedInfo { |
| 106 public String signature; |
| 107 public String country; |
| 108 public String date; |
| 109 public boolean isGzipCompressed; |
| 110 public byte[] seedData; |
| 111 } |
| 112 |
| 113 /** |
91 * Fetch the first run variations seed. | 114 * Fetch the first run variations seed. |
92 * @param restrictMode The restrict mode parameter to pass to the server via
a URL param. | 115 * @param restrictMode The restrict mode parameter to pass to the server via
a URL param. |
93 */ | 116 */ |
94 public void fetchSeed(String restrictMode) { | 117 public void fetchSeed(String restrictMode) { |
95 assert !ThreadUtils.runningOnUiThread(); | 118 assert !ThreadUtils.runningOnUiThread(); |
96 // Prevent multiple simultaneous fetches | 119 // Prevent multiple simultaneous fetches |
97 synchronized (sLock) { | 120 synchronized (sLock) { |
98 Context context = ContextUtils.getApplicationContext(); | |
99 SharedPreferences prefs = ContextUtils.getAppSharedPreferences(); | 121 SharedPreferences prefs = ContextUtils.getAppSharedPreferences(); |
100 // Early return if an attempt has already been made to fetch the see
d, even if it | 122 // Early return if an attempt has already been made to fetch the see
d, even if it |
101 // failed. Only attempt to get the initial Java seed once, since a f
ailure probably | 123 // failed. Only attempt to get the initial Java seed once, since a f
ailure probably |
102 // indicates a network problem that is unlikely to be resolved by a
second attempt. | 124 // indicates a network problem that is unlikely to be resolved by a
second attempt. |
103 // Note that VariationsSeedBridge.hasNativePref() is a pure Java fun
ction, reading an | 125 // Note that VariationsSeedBridge.hasNativePref() is a pure Java fun
ction, reading an |
104 // Android preference that is set when the seed is fetched by the na
tive code. | 126 // Android preference that is set when the seed is fetched by the na
tive code. |
105 if (prefs.getBoolean(VARIATIONS_INITIALIZED_PREF, false) | 127 if (prefs.getBoolean(VARIATIONS_INITIALIZED_PREF, false) |
106 || VariationsSeedBridge.hasNativePref()) { | 128 || VariationsSeedBridge.hasNativePref()) { |
107 return; | 129 return; |
108 } | 130 } |
109 downloadContent(context, restrictMode); | 131 |
| 132 try { |
| 133 SeedInfo info = downloadContent(VariationsPlatform.ANDROID, rest
rictMode); |
| 134 VariationsSeedBridge.setVariationsFirstRunSeed(info.seedData, in
fo.signature, |
| 135 info.country, info.date, info.isGzipCompressed); |
| 136 } catch (IOException e) { |
| 137 // Exceptions are handled and logged in the downloadContent meth
od, so we don't |
| 138 // need any exception handling here. The only reason we need a c
atch-statement here |
| 139 // is because those exceptions are re-thrown from downloadConten
t to skip the |
| 140 // normal logic flow within that method. |
| 141 } |
| 142 // VARIATIONS_INITIALIZED_PREF should still be set to true when exce
ptions occur |
110 prefs.edit().putBoolean(VARIATIONS_INITIALIZED_PREF, true).apply(); | 143 prefs.edit().putBoolean(VARIATIONS_INITIALIZED_PREF, true).apply(); |
111 } | 144 } |
112 } | 145 } |
113 | 146 |
114 private void recordFetchResultOrCode(int resultOrCode) { | 147 private void recordFetchResultOrCode(int resultOrCode) { |
115 SparseHistogramSample histogram = | 148 SparseHistogramSample histogram = |
116 new SparseHistogramSample("Variations.FirstRun.SeedFetchResult")
; | 149 new SparseHistogramSample("Variations.FirstRun.SeedFetchResult")
; |
117 histogram.record(resultOrCode); | 150 histogram.record(resultOrCode); |
118 } | 151 } |
119 | 152 |
120 private void recordSeedFetchTime(long timeDeltaMillis) { | 153 private void recordSeedFetchTime(long timeDeltaMillis) { |
121 Log.i(TAG, "Fetched first run seed in " + timeDeltaMillis + " ms"); | 154 Log.i(TAG, "Fetched first run seed in " + timeDeltaMillis + " ms"); |
122 TimesHistogramSample histogram = new TimesHistogramSample( | 155 TimesHistogramSample histogram = new TimesHistogramSample( |
123 "Variations.FirstRun.SeedFetchTime", TimeUnit.MILLISECONDS); | 156 "Variations.FirstRun.SeedFetchTime", TimeUnit.MILLISECONDS); |
124 histogram.record(timeDeltaMillis); | 157 histogram.record(timeDeltaMillis); |
125 } | 158 } |
126 | 159 |
127 private void recordSeedConnectTime(long timeDeltaMillis) { | 160 private void recordSeedConnectTime(long timeDeltaMillis) { |
128 TimesHistogramSample histogram = new TimesHistogramSample( | 161 TimesHistogramSample histogram = new TimesHistogramSample( |
129 "Variations.FirstRun.SeedConnectTime", TimeUnit.MILLISECONDS); | 162 "Variations.FirstRun.SeedConnectTime", TimeUnit.MILLISECONDS); |
130 histogram.record(timeDeltaMillis); | 163 histogram.record(timeDeltaMillis); |
131 } | 164 } |
132 | 165 |
133 private void downloadContent(Context context, String restrictMode) { | 166 /** |
| 167 * Download the variations seed data with platform and retrictMode. |
| 168 * @param platform The platform parameter to let server only return experime
nts which can be |
| 169 * run on that platform. |
| 170 * @param restrictMode The restrict mode parameter to pass to the server via
a URL param. |
| 171 * @throws SocketTimeoutException when fetching seed connection times out. |
| 172 * @throws UnknownHostException when fetching seed connection has an unknown
host. |
| 173 * @throws IOException when response code is not HTTP_OK or transmission fai
ls on the open |
| 174 * connection. |
| 175 */ |
| 176 public SeedInfo downloadContent(VariationsPlatform platform, String restrict
Mode) |
| 177 throws SocketTimeoutException, UnknownHostException, IOException { |
134 HttpURLConnection connection = null; | 178 HttpURLConnection connection = null; |
| 179 SeedInfo info = null; |
135 try { | 180 try { |
136 long startTimeMillis = SystemClock.elapsedRealtime(); | 181 long startTimeMillis = SystemClock.elapsedRealtime(); |
137 connection = getServerConnection(restrictMode); | 182 connection = getServerConnection(platform, restrictMode); |
138 connection.setReadTimeout(READ_TIMEOUT); | 183 connection.setReadTimeout(READ_TIMEOUT); |
139 connection.setConnectTimeout(REQUEST_TIMEOUT); | 184 connection.setConnectTimeout(REQUEST_TIMEOUT); |
140 connection.setDoInput(true); | 185 connection.setDoInput(true); |
141 connection.setRequestProperty("A-IM", "gzip"); | 186 connection.setRequestProperty("A-IM", "gzip"); |
142 connection.connect(); | 187 connection.connect(); |
143 int responseCode = connection.getResponseCode(); | 188 int responseCode = connection.getResponseCode(); |
144 recordFetchResultOrCode(responseCode); | 189 recordFetchResultOrCode(responseCode); |
145 if (responseCode != HttpURLConnection.HTTP_OK) { | 190 if (responseCode != HttpURLConnection.HTTP_OK) { |
146 Log.w(TAG, "Non-OK response code = %d", responseCode); | 191 String errorMsg = "Non-OK response code = " + responseCode; |
147 return; | 192 Log.w(TAG, errorMsg); |
| 193 throw new IOException(errorMsg); |
148 } | 194 } |
149 | 195 |
150 recordSeedConnectTime(SystemClock.elapsedRealtime() - startTimeMilli
s); | 196 recordSeedConnectTime(SystemClock.elapsedRealtime() - startTimeMilli
s); |
151 // Convert the InputStream into a byte array. | 197 |
152 byte[] rawSeed = getRawSeed(connection); | 198 info = new SeedInfo(); |
153 String signature = getHeaderFieldOrEmpty(connection, "X-Seed-Signatu
re"); | 199 info.seedData = getRawSeed(connection); |
154 String country = getHeaderFieldOrEmpty(connection, "X-Country"); | 200 info.signature = getHeaderFieldOrEmpty(connection, "X-Seed-Signature
"); |
155 String date = getHeaderFieldOrEmpty(connection, "Date"); | 201 info.country = getHeaderFieldOrEmpty(connection, "X-Country"); |
156 boolean isGzipCompressed = getHeaderFieldOrEmpty(connection, "IM").e
quals("gzip"); | 202 info.date = getHeaderFieldOrEmpty(connection, "Date"); |
157 VariationsSeedBridge.setVariationsFirstRunSeed( | 203 info.isGzipCompressed = getHeaderFieldOrEmpty(connection, "IM").equa
ls("gzip"); |
158 rawSeed, signature, country, date, isGzipCompressed); | |
159 recordSeedFetchTime(SystemClock.elapsedRealtime() - startTimeMillis)
; | 204 recordSeedFetchTime(SystemClock.elapsedRealtime() - startTimeMillis)
; |
| 205 return info; |
160 } catch (SocketTimeoutException e) { | 206 } catch (SocketTimeoutException e) { |
161 recordFetchResultOrCode(SEED_FETCH_RESULT_TIMEOUT); | 207 recordFetchResultOrCode(SEED_FETCH_RESULT_TIMEOUT); |
162 Log.w(TAG, "SocketTimeoutException fetching first run seed: ", e); | 208 Log.w(TAG, "SocketTimeoutException timeout when fetching variations
seed.", e); |
| 209 throw e; |
163 } catch (UnknownHostException e) { | 210 } catch (UnknownHostException e) { |
164 recordFetchResultOrCode(SEED_FETCH_RESULT_UNKNOWN_HOST_EXCEPTION); | 211 recordFetchResultOrCode(SEED_FETCH_RESULT_UNKNOWN_HOST_EXCEPTION); |
165 Log.w(TAG, "UnknownHostException fetching first run seed: ", e); | 212 Log.w(TAG, "UnknownHostException unknown host when fetching variatio
ns seed.", e); |
| 213 throw e; |
166 } catch (IOException e) { | 214 } catch (IOException e) { |
167 recordFetchResultOrCode(SEED_FETCH_RESULT_IOEXCEPTION); | 215 recordFetchResultOrCode(SEED_FETCH_RESULT_IOEXCEPTION); |
168 Log.w(TAG, "IOException fetching first run seed: ", e); | 216 Log.w(TAG, "IOException when fetching variations seed.", e); |
| 217 throw e; |
169 } finally { | 218 } finally { |
170 if (connection != null) { | 219 if (connection != null) { |
171 connection.disconnect(); | 220 connection.disconnect(); |
172 } | 221 } |
173 } | 222 } |
174 } | 223 } |
175 | 224 |
176 private String getHeaderFieldOrEmpty(HttpURLConnection connection, String na
me) { | 225 private String getHeaderFieldOrEmpty(HttpURLConnection connection, String na
me) { |
177 String headerField = connection.getHeaderField(name); | 226 String headerField = connection.getHeaderField(name); |
178 if (headerField == null) { | 227 if (headerField == null) { |
(...skipping 17 matching lines...) Expand all Loading... |
196 private byte[] convertInputStreamToByteArray(InputStream inputStream) throws
IOException { | 245 private byte[] convertInputStreamToByteArray(InputStream inputStream) throws
IOException { |
197 ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream(); | 246 ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream(); |
198 byte[] buffer = new byte[BUFFER_SIZE]; | 247 byte[] buffer = new byte[BUFFER_SIZE]; |
199 int charactersReadCount = 0; | 248 int charactersReadCount = 0; |
200 while ((charactersReadCount = inputStream.read(buffer)) != -1) { | 249 while ((charactersReadCount = inputStream.read(buffer)) != -1) { |
201 byteBuffer.write(buffer, 0, charactersReadCount); | 250 byteBuffer.write(buffer, 0, charactersReadCount); |
202 } | 251 } |
203 return byteBuffer.toByteArray(); | 252 return byteBuffer.toByteArray(); |
204 } | 253 } |
205 } | 254 } |
OLD | NEW |